• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2012 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "base/ios/scoped_critical_action.h"
6
7#import <UIKit/UIKit.h>
8#include <float.h>
9
10#include <atomic>
11#include <string_view>
12
13#include "base/ios/ios_util.h"
14#include "base/logging.h"
15#include "base/memory/ref_counted.h"
16#include "base/memory/singleton.h"
17#include "base/metrics/histogram_macros.h"
18#include "base/metrics/user_metrics.h"
19#include "base/strings/sys_string_conversions.h"
20#include "base/synchronization/lock.h"
21
22namespace base::ios {
23
24BASE_FEATURE(kScopedCriticalActionSkipOnShutdown,
25             "ScopedCriticalActionSkipOnShutdown",
26             base::FEATURE_DISABLED_BY_DEFAULT);
27
28namespace {
29
30constexpr base::TimeDelta kMaxTaskReuseDelay = base::Seconds(3);
31
32// Used for unit-testing only.
33std::atomic<int> g_num_active_background_tasks_for_test{0};
34
35}  // namespace
36
37ScopedCriticalAction::ScopedCriticalAction(std::string_view task_name)
38    : task_handle_(ActiveBackgroundTaskCache::GetInstance()
39                       ->EnsureBackgroundTaskExistsWithName(task_name)) {}
40
41ScopedCriticalAction::~ScopedCriticalAction() {
42  ActiveBackgroundTaskCache::GetInstance()->ReleaseHandle(task_handle_);
43}
44
45// static
46void ScopedCriticalAction::ApplicationWillTerminate() {
47  if (base::FeatureList::IsEnabled(kScopedCriticalActionSkipOnShutdown)) {
48    ActiveBackgroundTaskCache::GetInstance()->ApplicationWillTerminate();
49  }
50}
51
52// static
53void ScopedCriticalAction::ClearNumActiveBackgroundTasksForTest() {
54  g_num_active_background_tasks_for_test.store(0);
55}
56
57// static
58void ScopedCriticalAction::ResetApplicationWillTerminateForTest() {
59  ActiveBackgroundTaskCache::GetInstance()
60      ->ResetApplicationWillTerminateForTest();  // IN-TEST
61}
62
63// static
64int ScopedCriticalAction::GetNumActiveBackgroundTasksForTest() {
65  return g_num_active_background_tasks_for_test.load();
66}
67
68ScopedCriticalAction::Core::Core()
69    : background_task_id_(UIBackgroundTaskInvalid) {}
70
71ScopedCriticalAction::Core::~Core() {
72  DCHECK_EQ(background_task_id_, UIBackgroundTaskInvalid);
73}
74
75// This implementation calls |beginBackgroundTaskWithName:expirationHandler:|
76// when instantiated and |endBackgroundTask:| when destroyed, creating a scope
77// whose execution will continue (temporarily) even after the app is
78// backgrounded.
79// static
80void ScopedCriticalAction::Core::StartBackgroundTask(
81    scoped_refptr<Core> core,
82    std::string_view task_name) {
83  UIApplication* application = UIApplication.sharedApplication;
84  if (!application) {
85    return;
86  }
87
88  AutoLock lock_scope(core->background_task_id_lock_);
89  if (core->background_task_id_ != UIBackgroundTaskInvalid) {
90    // Already started.
91    return;
92  }
93
94  NSString* task_string =
95      !task_name.empty() ? base::SysUTF8ToNSString(task_name) : nil;
96  core->background_task_id_ = [application
97      beginBackgroundTaskWithName:task_string
98                expirationHandler:^{
99                  DLOG(WARNING)
100                      << "Background task with name <"
101                      << base::SysNSStringToUTF8(task_string) << "> and with "
102                      << "id " << core->background_task_id_ << " expired.";
103                  // Note if |endBackgroundTask:| is not called for each task
104                  // before time expires, the system kills the application.
105                  EndBackgroundTask(core);
106                }];
107
108  if (core->background_task_id_ == UIBackgroundTaskInvalid) {
109    DLOG(WARNING) << "beginBackgroundTaskWithName:<" << task_name << "> "
110                  << "expirationHandler: returned an invalid ID";
111  } else {
112    VLOG(3) << "Beginning background task <" << task_name << "> with id "
113            << core->background_task_id_;
114    g_num_active_background_tasks_for_test.fetch_add(1,
115                                                     std::memory_order_relaxed);
116  }
117}
118
119// static
120void ScopedCriticalAction::Core::EndBackgroundTask(scoped_refptr<Core> core) {
121  UIBackgroundTaskIdentifier task_id;
122  {
123    AutoLock lock_scope(core->background_task_id_lock_);
124    if (core->background_task_id_ == UIBackgroundTaskInvalid) {
125      // Never started successfully or already ended.
126      return;
127    }
128    task_id =
129        static_cast<UIBackgroundTaskIdentifier>(core->background_task_id_);
130    core->background_task_id_ = UIBackgroundTaskInvalid;
131  }
132
133  VLOG(3) << "Ending background task with id " << task_id;
134  [[UIApplication sharedApplication] endBackgroundTask:task_id];
135  g_num_active_background_tasks_for_test.fetch_sub(1,
136                                                   std::memory_order_relaxed);
137}
138
139ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry::
140    InternalEntry() = default;
141
142ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry::
143    ~InternalEntry() = default;
144
145ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry::InternalEntry(
146    InternalEntry&&) = default;
147
148ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry&
149ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry::operator=(
150    InternalEntry&&) = default;
151
152// static
153ScopedCriticalAction::ActiveBackgroundTaskCache*
154ScopedCriticalAction::ActiveBackgroundTaskCache::GetInstance() {
155  return base::Singleton<
156      ActiveBackgroundTaskCache,
157      base::LeakySingletonTraits<ActiveBackgroundTaskCache>>::get();
158}
159
160ScopedCriticalAction::ActiveBackgroundTaskCache::ActiveBackgroundTaskCache() =
161    default;
162
163ScopedCriticalAction::ActiveBackgroundTaskCache::~ActiveBackgroundTaskCache() =
164    default;
165
166ScopedCriticalAction::ActiveBackgroundTaskCache::Handle ScopedCriticalAction::
167    ActiveBackgroundTaskCache::EnsureBackgroundTaskExistsWithName(
168        std::string_view task_name) {
169  const base::TimeTicks now = base::TimeTicks::Now();
170  const base::TimeTicks min_reusable_time = now - kMaxTaskReuseDelay;
171  NameAndTime min_reusable_key{task_name, min_reusable_time};
172
173  Handle handle;
174  {
175    AutoLock lock_scope(entries_map_lock_);
176    auto lower_it = entries_map_.lower_bound(min_reusable_key);
177
178    if (lower_it != entries_map_.end() && lower_it->first.first == task_name) {
179      // A reusable Core instance exists, with the same name and created
180      // recently enough to warrant reuse.
181      DCHECK_GE(lower_it->first.second, min_reusable_time);
182      handle = lower_it;
183    } else {
184      // No reusable entry exists, so a new entry needs to be created.
185      auto it = entries_map_.emplace_hint(
186          lower_it, NameAndTime{std::move(min_reusable_key.first), now},
187          InternalEntry{});
188      DCHECK_EQ(it->first.second, now);
189      DCHECK(!it->second.core);
190      handle = it;
191      handle->second.core = MakeRefCounted<Core>();
192    }
193
194    // This guarantees a non-zero counter and hence the deletion of this map
195    // entry during this function body, even after the lock is released.
196    ++handle->second.num_active_handles;
197  }
198
199  // If this call didn't newly-create a Core instance, the call to
200  // StartBackgroundTask() is almost certainly (barring race conditions)
201  // unnecessary. It is however harmless to invoke it twice.
202  if (!application_is_terminating_) {
203    Core::StartBackgroundTask(handle->second.core, task_name);
204  }
205
206  return handle;
207}
208
209void ScopedCriticalAction::ActiveBackgroundTaskCache::ReleaseHandle(
210    Handle handle) {
211  scoped_refptr<Core> background_task_to_end;
212
213  {
214    AutoLock lock_scope(entries_map_lock_);
215    --handle->second.num_active_handles;
216    if (handle->second.num_active_handles == 0) {
217      // Move to |background_task_to_end| so the global lock is released before
218      // invoking EndBackgroundTask() which is expensive.
219      background_task_to_end = std::move(handle->second.core);
220      entries_map_.erase(handle);
221    }
222  }
223
224  // Note that at this point another, since the global lock was released,
225  // another task could have started with the same name, but this harmless.
226  if (background_task_to_end != nullptr) {
227    Core::EndBackgroundTask(std::move(background_task_to_end));
228  }
229}
230
231void ScopedCriticalAction::ActiveBackgroundTaskCache::
232    ApplicationWillTerminate() {
233  application_is_terminating_ = true;
234}
235
236void ScopedCriticalAction::ActiveBackgroundTaskCache::
237    ResetApplicationWillTerminateForTest() {
238  application_is_terminating_ = false;
239}
240
241}  // namespace base::ios
242