xref: /aosp_15_r20/external/cronet/base/ios/scoped_critical_action.mm (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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 {
23namespace {
24
25constexpr base::TimeDelta kMaxTaskReuseDelay = base::Seconds(3);
26
27// Used for unit-testing only.
28std::atomic<int> g_num_active_background_tasks_for_test{0};
29
30}  // namespace
31
32ScopedCriticalAction::ScopedCriticalAction(std::string_view task_name)
33    : task_handle_(ActiveBackgroundTaskCache::GetInstance()
34                       ->EnsureBackgroundTaskExistsWithName(task_name)) {}
35
36ScopedCriticalAction::~ScopedCriticalAction() {
37  ActiveBackgroundTaskCache::GetInstance()->ReleaseHandle(task_handle_);
38}
39
40// static
41void ScopedCriticalAction::ClearNumActiveBackgroundTasksForTest() {
42  g_num_active_background_tasks_for_test.store(0);
43}
44
45// static
46int ScopedCriticalAction::GetNumActiveBackgroundTasksForTest() {
47  return g_num_active_background_tasks_for_test.load();
48}
49
50ScopedCriticalAction::Core::Core()
51    : background_task_id_(UIBackgroundTaskInvalid) {}
52
53ScopedCriticalAction::Core::~Core() {
54  DCHECK_EQ(background_task_id_, UIBackgroundTaskInvalid);
55}
56
57// This implementation calls |beginBackgroundTaskWithName:expirationHandler:|
58// when instantiated and |endBackgroundTask:| when destroyed, creating a scope
59// whose execution will continue (temporarily) even after the app is
60// backgrounded.
61// static
62void ScopedCriticalAction::Core::StartBackgroundTask(
63    scoped_refptr<Core> core,
64    std::string_view task_name) {
65  UIApplication* application = UIApplication.sharedApplication;
66  if (!application) {
67    return;
68  }
69
70  AutoLock lock_scope(core->background_task_id_lock_);
71  if (core->background_task_id_ != UIBackgroundTaskInvalid) {
72    // Already started.
73    return;
74  }
75
76  NSString* task_string =
77      !task_name.empty() ? base::SysUTF8ToNSString(task_name) : nil;
78  core->background_task_id_ = [application
79      beginBackgroundTaskWithName:task_string
80                expirationHandler:^{
81                  DLOG(WARNING)
82                      << "Background task with name <"
83                      << base::SysNSStringToUTF8(task_string) << "> and with "
84                      << "id " << core->background_task_id_ << " expired.";
85                  // Note if |endBackgroundTask:| is not called for each task
86                  // before time expires, the system kills the application.
87                  EndBackgroundTask(core);
88                }];
89
90  if (core->background_task_id_ == UIBackgroundTaskInvalid) {
91    DLOG(WARNING) << "beginBackgroundTaskWithName:<" << task_name << "> "
92                  << "expirationHandler: returned an invalid ID";
93  } else {
94    VLOG(3) << "Beginning background task <" << task_name << "> with id "
95            << core->background_task_id_;
96    g_num_active_background_tasks_for_test.fetch_add(1,
97                                                     std::memory_order_relaxed);
98  }
99}
100
101// static
102void ScopedCriticalAction::Core::EndBackgroundTask(scoped_refptr<Core> core) {
103  UIBackgroundTaskIdentifier task_id;
104  {
105    AutoLock lock_scope(core->background_task_id_lock_);
106    if (core->background_task_id_ == UIBackgroundTaskInvalid) {
107      // Never started successfully or already ended.
108      return;
109    }
110    task_id =
111        static_cast<UIBackgroundTaskIdentifier>(core->background_task_id_);
112    core->background_task_id_ = UIBackgroundTaskInvalid;
113  }
114
115  VLOG(3) << "Ending background task with id " << task_id;
116  [[UIApplication sharedApplication] endBackgroundTask:task_id];
117  g_num_active_background_tasks_for_test.fetch_sub(1,
118                                                   std::memory_order_relaxed);
119}
120
121ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry::
122    InternalEntry() = default;
123
124ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry::
125    ~InternalEntry() = default;
126
127ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry::InternalEntry(
128    InternalEntry&&) = default;
129
130ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry&
131ScopedCriticalAction::ActiveBackgroundTaskCache::InternalEntry::operator=(
132    InternalEntry&&) = default;
133
134// static
135ScopedCriticalAction::ActiveBackgroundTaskCache*
136ScopedCriticalAction::ActiveBackgroundTaskCache::GetInstance() {
137  return base::Singleton<
138      ActiveBackgroundTaskCache,
139      base::LeakySingletonTraits<ActiveBackgroundTaskCache>>::get();
140}
141
142ScopedCriticalAction::ActiveBackgroundTaskCache::ActiveBackgroundTaskCache() =
143    default;
144
145ScopedCriticalAction::ActiveBackgroundTaskCache::~ActiveBackgroundTaskCache() =
146    default;
147
148ScopedCriticalAction::ActiveBackgroundTaskCache::Handle ScopedCriticalAction::
149    ActiveBackgroundTaskCache::EnsureBackgroundTaskExistsWithName(
150        std::string_view task_name) {
151  const base::TimeTicks now = base::TimeTicks::Now();
152  const base::TimeTicks min_reusable_time = now - kMaxTaskReuseDelay;
153  NameAndTime min_reusable_key{task_name, min_reusable_time};
154
155  Handle handle;
156  {
157    AutoLock lock_scope(entries_map_lock_);
158    auto lower_it = entries_map_.lower_bound(min_reusable_key);
159
160    if (lower_it != entries_map_.end() && lower_it->first.first == task_name) {
161      // A reusable Core instance exists, with the same name and created
162      // recently enough to warrant reuse.
163      DCHECK_GE(lower_it->first.second, min_reusable_time);
164      handle = lower_it;
165    } else {
166      // No reusable entry exists, so a new entry needs to be created.
167      auto it = entries_map_.emplace_hint(
168          lower_it, NameAndTime{std::move(min_reusable_key.first), now},
169          InternalEntry{});
170      DCHECK_EQ(it->first.second, now);
171      DCHECK(!it->second.core);
172      handle = it;
173      handle->second.core = MakeRefCounted<Core>();
174    }
175
176    // This guarantees a non-zero counter and hence the deletion of this map
177    // entry during this function body, even after the lock is released.
178    ++handle->second.num_active_handles;
179  }
180
181  // If this call didn't newly-create a Core instance, the call to
182  // StartBackgroundTask() is almost certainly (barring race conditions)
183  // unnecessary. It is however harmless to invoke it twice.
184  Core::StartBackgroundTask(handle->second.core, task_name);
185
186  return handle;
187}
188
189void ScopedCriticalAction::ActiveBackgroundTaskCache::ReleaseHandle(
190    Handle handle) {
191  scoped_refptr<Core> background_task_to_end;
192
193  {
194    AutoLock lock_scope(entries_map_lock_);
195    --handle->second.num_active_handles;
196    if (handle->second.num_active_handles == 0) {
197      // Move to |background_task_to_end| so the global lock is released before
198      // invoking EndBackgroundTask() which is expensive.
199      background_task_to_end = std::move(handle->second.core);
200      entries_map_.erase(handle);
201    }
202  }
203
204  // Note that at this point another, since the global lock was released,
205  // another task could have started with the same name, but this harmless.
206  if (background_task_to_end != nullptr) {
207    Core::EndBackgroundTask(std::move(background_task_to_end));
208  }
209}
210
211}  // namespace base::ios
212