@img/sharp-libvips-dev-wasm32 1.3.0-rc.6 → 1.3.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +23 -23
  2. package/include/expat.h +14 -2
  3. package/include/expat_config.h +6 -3
  4. package/include/expat_external.h +3 -2
  5. package/include/glib-2.0/glib/gvarianttype.h +16 -16
  6. package/include/glib-2.0/glib/gversionmacros.h +13 -0
  7. package/include/hwy/aligned_allocator.h +44 -2
  8. package/include/hwy/auto_tune.h +22 -6
  9. package/include/hwy/base.h +60 -90
  10. package/include/hwy/bit_set.h +410 -0
  11. package/include/hwy/cache_control.h +3 -2
  12. package/include/hwy/detect_compiler_arch.h +134 -14
  13. package/include/hwy/detect_targets.h +82 -36
  14. package/include/hwy/highway.h +15 -0
  15. package/include/hwy/nanobenchmark.h +12 -3
  16. package/include/hwy/ops/arm_neon-inl.h +96 -25
  17. package/include/hwy/ops/arm_sve-inl.h +250 -95
  18. package/include/hwy/ops/emu128-inl.h +12 -7
  19. package/include/hwy/ops/generic_ops-inl.h +271 -131
  20. package/include/hwy/ops/loongarch_lasx-inl.h +31 -9
  21. package/include/hwy/ops/loongarch_lsx-inl.h +33 -12
  22. package/include/hwy/ops/ppc_vsx-inl.h +99 -19
  23. package/include/hwy/ops/rvv-inl.h +239 -130
  24. package/include/hwy/ops/scalar-inl.h +22 -7
  25. package/include/hwy/ops/set_macros-inl.h +163 -107
  26. package/include/hwy/ops/wasm_128-inl.h +13 -22
  27. package/include/hwy/ops/x86_128-inl.h +223 -120
  28. package/include/hwy/ops/x86_256-inl.h +70 -134
  29. package/include/hwy/ops/x86_512-inl.h +130 -134
  30. package/include/hwy/print-inl.h +2 -0
  31. package/include/hwy/profiler.h +355 -264
  32. package/include/hwy/robust_statistics.h +3 -1
  33. package/include/hwy/timer.h +44 -0
  34. package/include/lcms2.h +34 -4
  35. package/include/libexif/exif-utils.h +1 -1
  36. package/include/libheif/heif_sequences.h +12 -2
  37. package/include/libheif/heif_uncompressed.h +8 -0
  38. package/include/libpng16/png.h +7 -7
  39. package/include/libpng16/pngconf.h +1 -1
  40. package/include/libpng16/pnglibconf.h +1 -1
  41. package/include/png.h +7 -7
  42. package/include/pngconf.h +1 -1
  43. package/include/pnglibconf.h +1 -1
  44. package/include/tiffconf.h +9 -10
  45. package/include/tiffio.h +10 -9
  46. package/include/tiffvers.h +1 -1
  47. package/include/ultrahdr_api.h +5 -11
  48. package/lib/glib-2.0/include/glibconfig.h +1 -1
  49. package/lib/libaom.a +0 -0
  50. package/lib/libcgif.a +0 -0
  51. package/lib/libexif.a +0 -0
  52. package/lib/libexif.la +1 -1
  53. package/lib/libexpat.a +0 -0
  54. package/lib/libexpat.la +3 -3
  55. package/lib/libffi.a +0 -0
  56. package/lib/libgio-2.0.a +0 -0
  57. package/lib/libglib-2.0.a +0 -0
  58. package/lib/libgmodule-2.0.a +0 -0
  59. package/lib/libgobject-2.0.a +0 -0
  60. package/lib/libgthread-2.0.a +0 -0
  61. package/lib/libheif.a +0 -0
  62. package/lib/libhwy.a +0 -0
  63. package/lib/libimagequant.a +0 -0
  64. package/lib/libjpeg.a +0 -0
  65. package/lib/liblcms2.a +0 -0
  66. package/lib/libpng.a +0 -0
  67. package/lib/libpng.la +2 -2
  68. package/lib/libpng16.a +0 -0
  69. package/lib/libpng16.la +2 -2
  70. package/lib/libresvg.a +0 -0
  71. package/lib/libsharpyuv.a +0 -0
  72. package/lib/libtiff.a +0 -0
  73. package/lib/libuhdr.a +0 -0
  74. package/lib/libvips-cpp.a +0 -0
  75. package/lib/libvips.a +0 -0
  76. package/lib/libwebp.a +0 -0
  77. package/lib/libwebpdecoder.a +0 -0
  78. package/lib/libwebpdemux.a +0 -0
  79. package/lib/libwebpmux.a +0 -0
  80. package/lib/libz.a +0 -0
  81. package/lib/pkgconfig/expat.pc +1 -1
  82. package/lib/pkgconfig/gio-2.0.pc +1 -1
  83. package/lib/pkgconfig/glib-2.0.pc +1 -1
  84. package/lib/pkgconfig/gmodule-2.0.pc +1 -1
  85. package/lib/pkgconfig/gmodule-export-2.0.pc +1 -1
  86. package/lib/pkgconfig/gmodule-no-export-2.0.pc +1 -1
  87. package/lib/pkgconfig/gobject-2.0.pc +1 -1
  88. package/lib/pkgconfig/gthread-2.0.pc +1 -1
  89. package/lib/pkgconfig/lcms2.pc +1 -1
  90. package/lib/pkgconfig/libexif.pc +1 -1
  91. package/lib/pkgconfig/libhwy.pc +3 -4
  92. package/lib/pkgconfig/libpng.pc +1 -1
  93. package/lib/pkgconfig/libpng16.pc +1 -1
  94. package/lib/pkgconfig/libsharpyuv.pc +1 -1
  95. package/lib/pkgconfig/libtiff-4.pc +2 -2
  96. package/lib/pkgconfig/libwebp.pc +1 -1
  97. package/lib/pkgconfig/libwebpdecoder.pc +11 -0
  98. package/lib/pkgconfig/libwebpmux.pc +1 -1
  99. package/package.json +1 -1
  100. package/versions.json +10 -10
  101. package/lib/libsharpyuv.la +0 -41
  102. package/lib/libtiff.la +0 -41
  103. package/lib/libwebp.la +0 -41
  104. package/lib/libwebpdemux.la +0 -41
  105. package/lib/libwebpmux.la +0 -41
@@ -17,21 +17,29 @@
17
17
 
18
18
  #include <stddef.h>
19
19
  #include <stdint.h>
20
+ #include <string.h> // strcmp, strlen
21
+
22
+ #include <atomic>
23
+ #include <functional>
20
24
 
25
+ #include "hwy/base.h"
21
26
  #include "hwy/highway_export.h"
22
27
 
23
28
  // High precision, low overhead time measurements. Returns exact call counts and
24
29
  // total elapsed time for user-defined 'zones' (code regions, i.e. C++ scopes).
25
30
  //
26
31
  // Uses RAII to capture begin/end timestamps, with user-specified zone names:
27
- // { PROFILER_ZONE("name"); /*code*/ } or
28
- // the name of the current function:
29
- // void FuncToMeasure() { PROFILER_FUNC; /*code*/ }.
30
- // You can reduce the overhead by passing a thread ID:
31
- // `PROFILER_ZONE2(thread, name)`. The new and preferred API also allows
32
- // passing flags, such as requesting inclusive time:
32
+ // `{ PROFILER_ZONE("name"); /*code*/ }` or the name of the current function:
33
+ // `void FuncToMeasure() { PROFILER_FUNC; /*code*/ }`.
34
+ //
35
+ // You can reduce the overhead by passing `global_idx`, which can be taken from
36
+ // the argument to the `ThreadPool::Run` lambda (if the pool was constructed
37
+ // with non-default `PoolWorkerMapping`), or from a saved copy of the
38
+ // thread-local `Profiler::Thread`: `PROFILER_ZONE2(global_idx, name)`.
39
+ //
40
+ // The preferred API allows passing flags, such as requesting inclusive time:
33
41
  // `static const auto zone = profiler.AddZone("name", flags);` and then
34
- // `PROFILER_ZONE3(profiler, thread, zone)`.
42
+ // `PROFILER_ZONE3(profiler, global_idx, zone)`.
35
43
  //
36
44
  // After all threads exit all zones, call `Profiler::Get().PrintResults()` to
37
45
  // print call counts and average durations [CPU cycles] to stdout, sorted in
@@ -44,14 +52,12 @@
44
52
 
45
53
  #if PROFILER_ENABLED
46
54
  #include <stdio.h>
47
- #include <string.h> // strcmp, strlen
48
55
 
49
56
  #include <algorithm> // std::sort
50
- #include <atomic>
57
+ #include <utility>
51
58
  #include <vector>
52
59
 
53
60
  #include "hwy/aligned_allocator.h"
54
- #include "hwy/base.h"
55
61
  #include "hwy/bit_set.h"
56
62
  #include "hwy/timer.h"
57
63
  #endif // PROFILER_ENABLED
@@ -60,8 +66,8 @@ namespace hwy {
60
66
 
61
67
  // Flags: we want type-safety (enum class) to catch mistakes such as confusing
62
68
  // zone with flags. Base type (`uint32_t`) ensures it is safe to cast. Defined
63
- // outside the `#if` because callers pass them to `PROFILER_ZONE3`. Keep in
64
- // sync with `kNumFlags` below.
69
+ // outside the `#if` because callers pass them to `PROFILER_ZONE3`. When adding
70
+ // flags, also update `kNumFlags` and `ChildTotalMask`.
65
71
  enum class ProfilerFlags : uint32_t {
66
72
  kDefault = 0,
67
73
  // The zone should report cumulative time, including all child zones. If not
@@ -69,6 +75,78 @@ enum class ProfilerFlags : uint32_t {
69
75
  kInclusive = 1
70
76
  };
71
77
 
78
+ // Called during `PrintResults` to print results from other modules.
79
+ using ProfilerFunc = std::function<void(void)>;
80
+
81
+ template <size_t kMaxStrings>
82
+ class StringTable {
83
+ static constexpr std::memory_order kRelaxed = std::memory_order_relaxed;
84
+ static constexpr std::memory_order kAcq = std::memory_order_acquire;
85
+ static constexpr std::memory_order kRel = std::memory_order_release;
86
+
87
+ public:
88
+ // Returns a copy of the `name` passed to `Add` that returned the
89
+ // given `idx`.
90
+ const char* Name(size_t idx) const {
91
+ // `kAcq` so that the string contents are also visible after the pointer is
92
+ // published via `kRelease` store.
93
+ return ptrs_[idx].load(kAcq);
94
+ }
95
+
96
+ // Returns `idx < kMaxStrings`. Can be called concurrently. Calls with the
97
+ // same `name` return the same `idx`.
98
+ size_t Add(const char* name) {
99
+ // Linear search if it already exists. `kAcq` ensures we see prior stores.
100
+ const size_t num_strings = next_ptr_.load(kAcq);
101
+ HWY_ASSERT(num_strings < kMaxStrings);
102
+ for (size_t idx = 1; idx < num_strings; ++idx) {
103
+ const char* existing = ptrs_[idx].load(kAcq);
104
+ // `next_ptr_` was published after writing `ptr_`, hence it is non-null.
105
+ HWY_ASSERT(existing != nullptr);
106
+ if (HWY_UNLIKELY(!strcmp(existing, name))) {
107
+ return idx;
108
+ }
109
+ }
110
+
111
+ // Copy `name` into `chars_` before publishing the pointer.
112
+ const size_t len = strlen(name) + 1;
113
+ const size_t pos = next_char_.fetch_add(len, kRelaxed);
114
+ HWY_ASSERT(pos + len <= sizeof(chars_));
115
+ strcpy(chars_ + pos, name); // NOLINT
116
+
117
+ for (;;) {
118
+ size_t idx = next_ptr_.load(kRelaxed);
119
+ HWY_ASSERT(idx < kMaxStrings);
120
+
121
+ // Attempt to claim the next `idx` via CAS.
122
+ const char* expected = nullptr;
123
+ if (HWY_LIKELY(ptrs_[idx].compare_exchange_weak(expected, chars_ + pos,
124
+ kRel, kRelaxed))) {
125
+ // Publish the new count and make the `ptrs_` write visible.
126
+ next_ptr_.store(idx + 1, kRel);
127
+ HWY_DASSERT(!strcmp(Name(idx), name));
128
+ return idx;
129
+ }
130
+
131
+ // We lost the race. `expected` has been updated.
132
+ if (HWY_UNLIKELY(!strcmp(expected, name))) {
133
+ // Done, another thread added the same name. Note that we waste the
134
+ // extra space in `chars_`, which is fine because it is rare.
135
+ HWY_DASSERT(!strcmp(Name(idx), name));
136
+ return idx;
137
+ }
138
+
139
+ // Other thread added a different name. Retry with the next slot.
140
+ }
141
+ }
142
+
143
+ private:
144
+ std::atomic<const char*> ptrs_[kMaxStrings];
145
+ std::atomic<size_t> next_ptr_{1}; // next idx
146
+ std::atomic<size_t> next_char_{0};
147
+ char chars_[kMaxStrings * 55];
148
+ };
149
+
72
150
  #if PROFILER_ENABLED
73
151
 
74
152
  // Implementation details.
@@ -78,13 +156,13 @@ HWY_INLINE_VAR constexpr size_t kNumFlags = 1;
78
156
 
79
157
  // Upper bounds for fixed-size data structures, guarded via HWY_DASSERT:
80
158
 
81
- // Maximum nesting of zones, chosen such that PerThread is 256 bytes.
159
+ // Maximum nesting of zones, chosen such that `PerWorker` is 256 bytes.
82
160
  HWY_INLINE_VAR constexpr size_t kMaxDepth = 13;
83
161
  // Reports with more than ~50 are anyway difficult to read.
84
162
  HWY_INLINE_VAR constexpr size_t kMaxZones = 128;
85
- // Upper bound on threads that call `InitThread`, and `thread` arguments. Note
86
- // that fiber libraries can spawn hundreds of threads. Enough for Turin cores.
87
- HWY_INLINE_VAR constexpr size_t kMaxThreads = 256;
163
+ // Upper bound on global worker_idx across all pools. Note that fiber libraries
164
+ // can spawn hundreds of threads. Turin has 128-192 cores.
165
+ HWY_INLINE_VAR constexpr size_t kMaxWorkers = 256;
88
166
 
89
167
  // Type-safe wrapper for zone index plus flags, returned by `AddZone`.
90
168
  class ZoneHandle {
@@ -119,8 +197,11 @@ class ZoneHandle {
119
197
 
120
198
  // Returns a mask to zero/ignore child totals for inclusive zones.
121
199
  uint64_t ChildTotalMask() const {
122
- // Without this function, clang tends to generate a branch.
123
- return IsInclusive() ? 0 : ~uint64_t{0};
200
+ // With a ternary operator, clang tends to generate a branch.
201
+ // return IsInclusive() ? 0 : ~uint64_t{0};
202
+ const uint32_t bit =
203
+ bits_ & static_cast<uint32_t>(ProfilerFlags::kInclusive);
204
+ return uint64_t{bit} - 1;
124
205
  }
125
206
 
126
207
  private:
@@ -128,48 +209,96 @@ class ZoneHandle {
128
209
  };
129
210
 
130
211
  // Storage for zone names.
131
- class Names {
132
- static constexpr std::memory_order kRel = std::memory_order_relaxed;
133
-
212
+ class Zones {
134
213
  public:
135
214
  // Returns a copy of the `name` passed to `AddZone` that returned the
136
215
  // given `zone`.
137
- const char* Get(ZoneHandle zone) const { return ptrs_[zone.ZoneIdx()]; }
216
+ const char* Name(ZoneHandle zone) const {
217
+ return strings_.Name(zone.ZoneIdx());
218
+ }
138
219
 
220
+ // Can be called concurrently. Calls with the same `name` return the same
221
+ // `ZoneHandle.ZoneIdx()`.
139
222
  ZoneHandle AddZone(const char* name, ProfilerFlags flags) {
140
- // Linear search whether it already exists.
141
- const size_t num_zones = next_ptr_.load(kRel);
142
- HWY_ASSERT(num_zones < kMaxZones);
143
- for (size_t zone_idx = 1; zone_idx < num_zones; ++zone_idx) {
144
- if (!strcmp(ptrs_[zone_idx], name)) {
145
- return ZoneHandle(zone_idx, flags);
223
+ return ZoneHandle(strings_.Add(name), flags);
224
+ }
225
+
226
+ private:
227
+ StringTable<kMaxZones> strings_;
228
+ };
229
+
230
+ // Allows other classes such as `ThreadPool` to register/unregister a function
231
+ // to call during `PrintResults`. This allows us to gather data from the worker
232
+ // threads without having to wait until they exit, and decouples the profiler
233
+ // from other modules. Thread-safe.
234
+ class Funcs {
235
+ static constexpr auto kAcq = std::memory_order_acquire;
236
+ static constexpr auto kRel = std::memory_order_release;
237
+
238
+ public:
239
+ // Can be called concurrently with distinct keys.
240
+ void Add(intptr_t key, ProfilerFunc func) {
241
+ HWY_ASSERT(key != 0 && key != kPending); // reserved values
242
+ HWY_ASSERT(func); // not empty
243
+
244
+ for (size_t i = 0; i < kMaxFuncs; ++i) {
245
+ intptr_t expected = 0;
246
+ // Lost a race with a concurrent `Add`, try the next slot.
247
+ if (!keys_[i].compare_exchange_strong(expected, kPending, kRel)) {
248
+ continue;
146
249
  }
250
+ // We own the slot: move func there.
251
+ funcs_[i] = std::move(func);
252
+ keys_[i].store(key, kRel); // publishes the `func` write.
253
+ return;
147
254
  }
148
255
 
149
- // Reserve the next `zone_idx` (index in `ptrs_`).
150
- const size_t zone_idx = next_ptr_.fetch_add(1, kRel);
151
-
152
- // Copy into `name` into `chars_`.
153
- const size_t len = strlen(name) + 1;
154
- const size_t pos = next_char_.fetch_add(len, kRel);
155
- HWY_ASSERT(pos + len <= sizeof(chars_));
156
- strcpy(chars_ + pos, name); // NOLINT
256
+ HWY_ABORT("Funcs::Add: no free slot, increase kMaxFuncs.");
257
+ }
258
+
259
+ // Can be called concurrently with distinct keys. It is an error to call this
260
+ // without a prior `Add` of the same key.
261
+ void Remove(intptr_t key) {
262
+ HWY_ASSERT(key != 0 && key != kPending); // reserved values
263
+
264
+ for (size_t i = 0; i < kMaxFuncs; ++i) {
265
+ intptr_t actual = keys_[i].load(kAcq);
266
+ if (actual == key) {
267
+ // In general, concurrent removal is fine, but in this specific context,
268
+ // owners are expected to remove their key exactly once, from the same
269
+ // thread that added it. In that case, CAS should not fail.
270
+ if (!keys_[i].compare_exchange_strong(actual, kPending, kRel)) {
271
+ HWY_WARN("Funcs: CAS failed, why is there a concurrent Remove?");
272
+ }
273
+ funcs_[i] = ProfilerFunc();
274
+ keys_[i].store(0, kRel); // publishes the `func` write.
275
+ return;
276
+ }
277
+ }
278
+ HWY_ABORT("Funcs::Remove: failed to find key %p.",
279
+ reinterpret_cast<void*>(key));
280
+ }
157
281
 
158
- ptrs_[zone_idx] = chars_ + pos;
159
- const ZoneHandle zone(zone_idx, flags);
160
- HWY_DASSERT(!strcmp(Get(zone), name));
161
- return zone;
282
+ void CallAll() const {
283
+ for (size_t i = 0; i < kMaxFuncs; ++i) {
284
+ intptr_t key = keys_[i].load(kAcq); // ensures `funcs_` is visible.
285
+ // Safely handles concurrent Add/Remove.
286
+ if (key != 0 && key != kPending) {
287
+ funcs_[i]();
288
+ }
289
+ }
162
290
  }
163
291
 
164
292
  private:
165
- const char* ptrs_[kMaxZones];
166
- std::atomic<size_t> next_ptr_{1}; // next zone_idx
167
- char chars_[kMaxZones * 70];
168
- std::atomic<size_t> next_char_{0};
293
+ static constexpr size_t kMaxFuncs = 64;
294
+ static constexpr intptr_t kPending = -1;
295
+
296
+ ProfilerFunc funcs_[kMaxFuncs]; // non-atomic
297
+ std::atomic<intptr_t> keys_[kMaxFuncs] = {};
169
298
  };
170
299
 
171
- // Holds total duration and number of calls. "Which thread entered it" is
172
- // unnecessary because these are per-thread.
300
+ // Holds total duration and number of calls. Worker index is implicit in the
301
+ // index of this class within the `Accumulators` array.
173
302
  struct Accumulator {
174
303
  void Add(ZoneHandle new_zone, uint64_t self_duration) {
175
304
  duration += self_duration;
@@ -183,7 +312,7 @@ struct Accumulator {
183
312
  num_calls += 1;
184
313
  }
185
314
 
186
- void Assimilate(Accumulator& other) {
315
+ void Take(Accumulator& other) {
187
316
  duration += other.duration;
188
317
  other.duration = 0;
189
318
 
@@ -203,94 +332,38 @@ struct Accumulator {
203
332
  };
204
333
  static_assert(sizeof(Accumulator) == 16, "Wrong Accumulator size");
205
334
 
206
- // Modified from `hwy::BitSet4096`. Avoids the second-level `BitSet64`, because
207
- // we only need `kMaxZones` = 128.
208
- class ZoneSet {
209
- public:
210
- // No harm if `i` is already set.
211
- void Set(size_t i) {
212
- HWY_DASSERT(i < kMaxZones);
213
- const size_t idx = i / 64;
214
- const size_t mod = i % 64;
215
- bits_[idx].Set(mod);
216
- HWY_DASSERT(Get(i));
217
- }
218
-
219
- void Clear(size_t i) {
220
- HWY_DASSERT(i < kMaxZones);
221
- const size_t idx = i / 64;
222
- const size_t mod = i % 64;
223
- bits_[idx].Clear(mod);
224
- HWY_DASSERT(!Get(i));
225
- }
226
-
227
- bool Get(size_t i) const {
228
- HWY_DASSERT(i < kMaxZones);
229
- const size_t idx = i / 64;
230
- const size_t mod = i % 64;
231
- return bits_[idx].Get(mod);
232
- }
233
-
234
- // Returns lowest i such that Get(i). Caller must ensure Any() beforehand!
235
- size_t First() const {
236
- HWY_DASSERT(bits_[0].Any() || bits_[1].Any());
237
- const size_t idx = bits_[0].Any() ? 0 : 1;
238
- return idx * 64 + bits_[idx].First();
239
- }
240
-
241
- // Calls `func(i)` for each `i` in the set. It is safe for `func` to modify
242
- // the set, but the current Foreach call is only affected if changing one of
243
- // the not yet visited BitSet64 for which Any() is true.
244
- template <class Func>
245
- void Foreach(const Func& func) const {
246
- bits_[0].Foreach([&func](size_t mod) { func(mod); });
247
- bits_[1].Foreach([&func](size_t mod) { func(64 + mod); });
248
- }
249
-
250
- size_t Count() const { return bits_[0].Count() + bits_[1].Count(); }
251
-
252
- private:
253
- static_assert(kMaxZones == 128, "Update ZoneSet");
254
- BitSet64 bits_[2];
255
- };
256
-
257
- // Modified from `ZoneSet`.
258
- class ThreadSet {
259
- public:
260
- // No harm if `i` is already set.
261
- void Set(size_t i) {
262
- HWY_DASSERT(i < kMaxThreads);
263
- const size_t idx = i / 64;
264
- const size_t mod = i % 64;
265
- bits_[idx].Set(mod);
266
- }
267
-
268
- size_t Count() const {
269
- size_t total = 0;
270
- for (const BitSet64& bits : bits_) {
271
- total += bits.Count();
272
- }
273
- return total;
274
- }
275
-
276
- private:
277
- BitSet64 bits_[DivCeil(kMaxThreads, size_t{64})];
278
- };
335
+ using ZoneSet = hwy::BitSet<kMaxZones>;
336
+ using WorkerSet = hwy::BitSet<kMaxWorkers>;
337
+ using AtomicWorkerSet = hwy::AtomicBitSet<kMaxWorkers>;
279
338
 
280
339
  // Durations are per-CPU, but end to end performance is defined by wall time.
281
340
  // Assuming fork-join parallelism, zones are entered by multiple threads
282
341
  // concurrently, which means the total number of unique threads is also the
283
342
  // degree of concurrency, so we can estimate wall time as CPU time divided by
284
- // the number of unique threads seen, tracked via `ThreadSet`.
343
+ // the number of unique threads seen. This is facilitated by unique `global_idx`
344
+ // passed in by callers, or taken from thread-local `GlobalIdx()`.
285
345
  //
286
346
  // We also want to support varying thread counts per call site, because the same
287
347
  // function/zone may be called from multiple pools. `EndRootRun` calls
288
- // `CountThreadsAndReset` after each top-level `ThreadPool::Run`, which
348
+ // `CountWorkersAndReset` after each top-level `ThreadPool::Run`, which
289
349
  // generates one data point summarized via descriptive statistics. Here we
290
- // implement a simpler version of `hwy::Stats` because we do not require
350
+ // implement a simpler version of `Stats` because we do not require
291
351
  // geomean/variance/kurtosis/skewness. Because concurrency is a small integer,
292
352
  // we can simply compute sums rather than online moments. There is also only one
293
- // instance across all threads, hence we do not require `Assimilate`.
353
+ // instance across all threads, hence we do not require a `Take`.
354
+ //
355
+ // Note that subsequently discovered prior work estimates the number of active
356
+ // and idle processors by updating atomic counters whenever they start/finish a
357
+ // task: https://homes.cs.washington.edu/~tom/pubs/quartz.pdf and "Effective
358
+ // performance measurement and analysis of multithreaded applications". We
359
+ // instead accumulate zone durations into per-thread storage.
360
+ // `CountWorkersAndReset` then checks how many were nonzero, which avoids
361
+ // expensive atomic updates and ensures accurate counts per-zone, rather than
362
+ // estimates of current activity at each sample.
363
+ // D. Vyukov's https://github.com/dvyukov/perf-load, also integrated into Linux
364
+ // perf, also corrects for parallelism without using atomic counters by tracing
365
+ // context switches. Note that we often pin threads, which avoids migrations,
366
+ // but reduces the number of context switch events to mainly preemptions.
294
367
  class ConcurrencyStats {
295
368
  public:
296
369
  ConcurrencyStats() { Reset(); }
@@ -324,45 +397,42 @@ class ConcurrencyStats {
324
397
  };
325
398
  static_assert(sizeof(ConcurrencyStats) == (8 + 3 * sizeof(size_t)), "");
326
399
 
327
- // Holds the final results across all threads, including `ConcurrencyStats`.
328
- // There is only one instance because this is updated by the main thread.
400
+ // Holds the final results across all threads, including `ConcurrencyStats`
401
+ // and `PoolStats`, updated/printed by the main thread.
329
402
  class Results {
330
403
  public:
331
- void Assimilate(const size_t thread, const size_t zone_idx,
332
- Accumulator& other) {
333
- HWY_DASSERT(thread < kMaxThreads);
404
+ void TakeAccumulator(const size_t global_idx, const size_t zone_idx,
405
+ Accumulator& other) {
406
+ HWY_DASSERT(global_idx < kMaxWorkers);
334
407
  HWY_DASSERT(zone_idx < kMaxZones);
335
408
  HWY_DASSERT(other.zone.ZoneIdx() == zone_idx);
336
409
 
337
410
  visited_zones_.Set(zone_idx);
338
- totals_[zone_idx].Assimilate(other);
339
- threads_[zone_idx].Set(thread);
411
+ totals_[zone_idx].Take(other);
412
+ workers_[zone_idx].Set(global_idx);
340
413
  }
341
414
 
342
415
  // Moves the total number of threads seen during the preceding root-level
343
416
  // `ThreadPool::Run` into one data point for `ConcurrencyStats`.
344
- void CountThreadsAndReset(const size_t zone_idx) {
417
+ void CountWorkersAndReset(const size_t zone_idx) {
345
418
  HWY_DASSERT(zone_idx < kMaxZones);
346
- const size_t num_threads = threads_[zone_idx].Count();
347
- // Although threads_[zone_idx] at one point was non-empty, it is reset
419
+ const size_t num_workers = workers_[zone_idx].Count();
420
+ // Although workers_[zone_idx] at one point was non-empty, it is reset
348
421
  // below, and so can be empty on the second call to this via `PrintResults`,
349
422
  // after one from `EndRootRun`. Do not add a data point if empty.
350
- if (num_threads != 0) {
351
- concurrency_[zone_idx].Notify(num_threads);
423
+ if (num_workers != 0) {
424
+ concurrency_[zone_idx].Notify(num_workers);
352
425
  }
353
- threads_[zone_idx] = ThreadSet();
426
+ workers_[zone_idx] = WorkerSet();
354
427
  }
355
428
 
356
- void CountThreadsAndReset() {
429
+ void CountWorkersAndReset() {
357
430
  visited_zones_.Foreach(
358
- [&](size_t zone_idx) { CountThreadsAndReset(zone_idx); });
431
+ [&](size_t zone_idx) { CountWorkersAndReset(zone_idx); });
359
432
  }
360
433
 
361
- void AddAnalysisTime(uint64_t t0) { analyze_elapsed_ += timer::Stop() - t0; }
362
-
363
- void Print(const Names& names) {
364
- const uint64_t t0 = timer::Start();
365
- const double inv_freq = 1.0 / platform::InvariantTicksPerSecond();
434
+ void PrintAndReset(const Zones& zones) {
435
+ const double inv_freq = 1.0 / hwy::platform::InvariantTicksPerSecond();
366
436
 
367
437
  // Sort by decreasing total (self) cost. `totals_` are sparse, so sort an
368
438
  // index vector instead.
@@ -371,11 +441,13 @@ class Results {
371
441
  visited_zones_.Foreach([&](size_t zone_idx) {
372
442
  indices.push_back(static_cast<uint32_t>(zone_idx));
373
443
  // In case the zone exited after `EndRootRun` and was not yet added.
374
- CountThreadsAndReset(zone_idx);
444
+ CountWorkersAndReset(zone_idx);
375
445
  });
376
446
  std::sort(indices.begin(), indices.end(), [&](uint32_t a, uint32_t b) {
377
447
  return totals_[a].duration > totals_[b].duration;
378
448
  });
449
+ printf(" %-40s: %10s x %15s / %5s (%5s %3s-%3s) = %9s\n", "Zone", "Calls",
450
+ "Cycles/Call", "Avg Count", "Count", "Min", "Max", "Wall Time(s)");
379
451
 
380
452
  for (uint32_t zone_idx : indices) {
381
453
  Accumulator& total = totals_[zone_idx]; // cleared after printing
@@ -391,29 +463,23 @@ class Results {
391
463
  // Avoid division by zero.
392
464
  const double concurrency_divisor = HWY_MAX(1.0, avg_concurrency);
393
465
  printf("%s%-40s: %10.0f x %15.0f / %5.1f (%5zu %3zu-%3zu) = %9.6f\n",
394
- total.zone.IsInclusive() ? "(I)" : " ", names.Get(total.zone),
466
+ total.zone.IsInclusive() ? "(I)" : " ", zones.Name(total.zone),
395
467
  static_cast<double>(total.num_calls), per_call, avg_concurrency,
396
468
  concurrency.Count(), concurrency.Min(), concurrency.Max(),
397
469
  duration * inv_freq / concurrency_divisor);
398
470
 
399
471
  total = Accumulator();
400
472
  concurrency.Reset();
401
- // `threads_` was already reset by `CountThreadsAndReset`.
473
+ // `workers_` was already reset by `CountWorkersAndReset`.
402
474
  }
403
475
  visited_zones_ = ZoneSet();
404
-
405
- AddAnalysisTime(t0);
406
- printf("Total analysis [s]: %f\n",
407
- static_cast<double>(analyze_elapsed_) * inv_freq);
408
- analyze_elapsed_ = 0;
409
476
  }
410
477
 
411
478
  private:
412
- uint64_t analyze_elapsed_ = 0;
413
479
  // Indicates which of the array entries are in use.
414
480
  ZoneSet visited_zones_;
415
481
  Accumulator totals_[kMaxZones];
416
- ThreadSet threads_[kMaxZones];
482
+ WorkerSet workers_[kMaxZones];
417
483
  ConcurrencyStats concurrency_[kMaxZones];
418
484
  };
419
485
 
@@ -421,33 +487,33 @@ class Results {
421
487
  // with frequency throttling disabled, this has a multimodal distribution,
422
488
  // including 32, 34, 48, 52, 59, 62.
423
489
  struct Overheads {
424
- uint32_t self = 0;
425
- uint32_t child = 0;
490
+ uint64_t self = 0;
491
+ uint64_t child = 0;
426
492
  };
427
- static_assert(sizeof(Overheads) == 8, "Wrong Overheads size");
493
+ static_assert(sizeof(Overheads) == 16, "Wrong Overheads size");
428
494
 
429
495
  class Accumulators {
430
496
  // We generally want to group threads together because they are often
431
497
  // accessed together during a zone, but also want to avoid threads sharing a
432
- // cache line. Hence interleave 8 zones per thread.
498
+ // cache line. Hence interleave 8 zones per worker.
433
499
  static constexpr size_t kPerLine = HWY_ALIGNMENT / sizeof(Accumulator);
434
500
 
435
501
  public:
436
- Accumulator& Get(const size_t thread, const size_t zone_idx) {
437
- HWY_DASSERT(thread < kMaxThreads);
502
+ Accumulator& Get(const size_t global_idx, const size_t zone_idx) {
503
+ HWY_DASSERT(global_idx < kMaxWorkers);
438
504
  HWY_DASSERT(zone_idx < kMaxZones);
439
505
  const size_t line = zone_idx / kPerLine;
440
506
  const size_t offset = zone_idx % kPerLine;
441
- return zones_[(line * kMaxThreads + thread) * kPerLine + offset];
507
+ return zones_[(line * kMaxWorkers + global_idx) * kPerLine + offset];
442
508
  }
443
509
 
444
510
  private:
445
- Accumulator zones_[kMaxZones * kMaxThreads];
511
+ Accumulator zones_[kMaxZones * kMaxWorkers];
446
512
  };
447
513
 
448
514
  // Reacts to zone enter/exit events. Builds a stack of active zones and
449
515
  // accumulates self/child duration for each.
450
- class PerThread {
516
+ class PerWorker {
451
517
  public:
452
518
  template <typename T>
453
519
  static T ClampedSubtract(const T minuend, const T subtrahend) {
@@ -467,12 +533,11 @@ class PerThread {
467
533
  t_enter_[depth] = t_enter;
468
534
  child_total_[1 + depth] = 0;
469
535
  depth_ = 1 + depth;
470
- HWY_IF_CONSTEXPR(HWY_IS_DEBUG_BUILD) { any_ = 1; }
471
536
  }
472
537
 
473
538
  // Exiting the most recently entered zone (top of stack).
474
- void Exit(const uint64_t t_exit, const size_t thread, const ZoneHandle zone,
475
- Accumulators& accumulators) {
539
+ void Exit(const uint64_t t_exit, const size_t global_idx,
540
+ const ZoneHandle zone, Accumulators& accumulators) {
476
541
  HWY_DASSERT(depth_ > 0);
477
542
  const size_t depth = depth_ - 1;
478
543
  const size_t zone_idx = zone.ZoneIdx();
@@ -484,8 +549,8 @@ class PerThread {
484
549
 
485
550
  const uint64_t self_duration = ClampedSubtract(
486
551
  duration, overheads_.self + overheads_.child + child_total);
487
- accumulators.Get(thread, zone_idx).Add(zone, self_duration);
488
- // For faster Assimilate() - not all zones are encountered.
552
+ accumulators.Get(global_idx, zone_idx).Add(zone, self_duration);
553
+ // For faster TakeAccumulator() - not all zones are encountered.
489
554
  visited_zones_.Set(zone_idx);
490
555
 
491
556
  // Adding this nested time to the parent's `child_total` will
@@ -495,11 +560,10 @@ class PerThread {
495
560
  depth_ = depth;
496
561
  }
497
562
 
498
- bool HadAnyZones() const { return HWY_IS_DEBUG_BUILD ? (any_ != 0) : false; }
499
-
500
563
  // Returns the duration of one enter/exit pair and resets all state. Called
501
564
  // via `DetectSelfOverhead`.
502
- uint64_t GetFirstDurationAndReset(size_t thread, Accumulators& accumulators) {
565
+ uint64_t GetFirstDurationAndReset(size_t global_idx,
566
+ Accumulators& accumulators) {
503
567
  HWY_DASSERT(depth_ == 0);
504
568
 
505
569
  HWY_DASSERT(visited_zones_.Count() == 1);
@@ -508,32 +572,28 @@ class PerThread {
508
572
  HWY_DASSERT(visited_zones_.Get(zone_idx));
509
573
  visited_zones_.Clear(zone_idx);
510
574
 
511
- Accumulator& zone = accumulators.Get(thread, zone_idx);
575
+ Accumulator& zone = accumulators.Get(global_idx, zone_idx);
512
576
  const uint64_t duration = zone.duration;
513
577
  zone = Accumulator();
514
578
  return duration;
515
579
  }
516
580
 
517
581
  // Adds all data to `results` and resets it here. Called from the main thread.
518
- void MoveTo(const size_t thread, Accumulators& accumulators,
582
+ void MoveTo(const size_t global_idx, Accumulators& accumulators,
519
583
  Results& results) {
520
- const uint64_t t0 = timer::Start();
521
-
522
584
  visited_zones_.Foreach([&](size_t zone_idx) {
523
- results.Assimilate(thread, zone_idx, accumulators.Get(thread, zone_idx));
585
+ results.TakeAccumulator(global_idx, zone_idx,
586
+ accumulators.Get(global_idx, zone_idx));
524
587
  });
525
588
  // OK to reset even if we have active zones, because we set `visited_zones_`
526
589
  // when exiting the zone.
527
590
  visited_zones_ = ZoneSet();
528
-
529
- results.AddAnalysisTime(t0);
530
591
  }
531
592
 
532
593
  private:
533
594
  // 40 bytes:
534
- ZoneSet visited_zones_; // Which `zones_` have been active on this thread.
595
+ ZoneSet visited_zones_; // Which `zones_` have been active on this worker.
535
596
  uint64_t depth_ = 0; // Current nesting level for active zones.
536
- uint64_t any_ = 0;
537
597
  Overheads overheads_;
538
598
 
539
599
  uint64_t t_enter_[kMaxDepth];
@@ -541,9 +601,8 @@ class PerThread {
541
601
  // Shifting by one avoids bounds-checks for depth_ = 0 (root zone).
542
602
  uint64_t child_total_[1 + kMaxDepth] = {0};
543
603
  };
544
-
545
604
  // Enables shift rather than multiplication.
546
- static_assert(sizeof(PerThread) == 256, "Wrong size");
605
+ static_assert(sizeof(PerWorker) == 256, "Wrong size");
547
606
 
548
607
  } // namespace profiler
549
608
 
@@ -551,37 +610,59 @@ class Profiler {
551
610
  public:
552
611
  static HWY_DLLEXPORT Profiler& Get();
553
612
 
554
- // Assigns the next counter value to the `thread_local` that `Thread` reads.
555
- // Must be called exactly once on each thread before any `PROFILER_ZONE`
556
- // (without a thread argument) are re-entered by multiple threads.
557
- // `Profiler()` takes care of calling this for the main thread. It is fine not
558
- // to call it for other threads as long as they only use `PROFILER_ZONE2` or
559
- // `PROFILER_ZONE3`, which take a thread argument and do not call `Thread`.
560
- static void InitThread() { s_thread = s_num_threads.fetch_add(1); }
613
+ // Returns `global_idx` from thread-local storage (0 for the main thread).
614
+ // Used by `PROFILER_ZONE/PROFILER_FUNC`. It is faster to instead pass the
615
+ // global_idx from `ThreadPool::Run` (if constructed with non-default
616
+ // `PoolWorkerMapping`) to `PROFILER_ZONE2/PROFILER_ZONE3`.
617
+ // DEPRECATED: use `GlobalIdx` instead.
618
+ static size_t Thread() { return s_global_idx; }
619
+ static size_t GlobalIdx() { return s_global_idx; }
620
+ // Must be called from all worker threads, and once also on the main thread,
621
+ // before any use of `PROFILER_ZONE/PROFILER_FUNC`.
622
+ static void SetGlobalIdx(size_t global_idx) { s_global_idx = global_idx; }
623
+
624
+ void ReserveWorker(size_t global_idx) {
625
+ HWY_ASSERT(!workers_reserved_.Get(global_idx));
626
+ workers_reserved_.Set(global_idx);
627
+ }
628
+
629
+ void FreeWorker(size_t global_idx) {
630
+ HWY_ASSERT(workers_reserved_.Get(global_idx));
631
+ workers_reserved_.Clear(global_idx);
632
+ }
561
633
 
562
- // Used by `PROFILER_ZONE/PROFILER_FUNC` to read the `thread` argument from
563
- // thread_local storage. It is faster to instead pass the ThreadPool `thread`
564
- // argument to `PROFILER_ZONE2/PROFILER_ZONE3`. Note that the main thread
565
- // calls `InitThread` first, hence its `Thread` returns zero, which matches
566
- // the main-first worker numbering used by `ThreadPool`.
567
- static size_t Thread() { return s_thread; }
634
+ // Called by `Zone` from any thread.
635
+ void Enter(uint64_t t_enter, size_t global_idx) {
636
+ GetWorker(global_idx).Enter(t_enter);
637
+ }
568
638
 
569
- // Speeds up `UpdateResults` by providing an upper bound on the number of
570
- // threads tighter than `profiler::kMaxThreads`. It is not required to be
571
- // tight, and threads less than this can still be unused.
572
- void SetMaxThreads(size_t max_threads) {
573
- HWY_ASSERT(max_threads <= profiler::kMaxThreads);
574
- max_threads_ = max_threads;
639
+ // Called by `~Zone` from any thread.
640
+ void Exit(uint64_t t_exit, size_t global_idx, profiler::ZoneHandle zone) {
641
+ GetWorker(global_idx).Exit(t_exit, global_idx, zone, accumulators_);
575
642
  }
576
643
 
577
- const char* Name(profiler::ZoneHandle zone) const { return names_.Get(zone); }
644
+ uint64_t GetFirstDurationAndReset(size_t global_idx) {
645
+ return GetWorker(global_idx)
646
+ .GetFirstDurationAndReset(global_idx, accumulators_);
647
+ }
648
+
649
+ const char* Name(profiler::ZoneHandle zone) const {
650
+ return zones_.Name(zone);
651
+ }
578
652
 
579
653
  // Copies `name` into the string table and returns its unique `zone`. Uses
580
654
  // linear search, which is fine because this is called during static init.
581
655
  // Called via static initializer and the result is passed to the `Zone` ctor.
582
656
  profiler::ZoneHandle AddZone(const char* name,
583
657
  ProfilerFlags flags = ProfilerFlags::kDefault) {
584
- return names_.AddZone(name, flags);
658
+ return zones_.AddZone(name, flags);
659
+ }
660
+
661
+ void AddFunc(void* owner, ProfilerFunc func) {
662
+ funcs_.Add(reinterpret_cast<intptr_t>(owner), func);
663
+ }
664
+ void RemoveFunc(void* owner) {
665
+ funcs_.Remove(reinterpret_cast<intptr_t>(owner));
585
666
  }
586
667
 
587
668
  // For reporting average concurrency. Called by `ThreadPool::Run` on the main
@@ -602,9 +683,9 @@ class Profiler {
602
683
  // broadcasts to "all cores", but there is no universal guarantee.
603
684
  //
604
685
  // Under the assumption that all concurrency is via our `ThreadPool`, we can
605
- // record all `thread` for each outermost (root) `ThreadPool::Run`. This
686
+ // record all `global_idx` for each outermost (root) `ThreadPool::Run`. This
606
687
  // collapses all nested pools into one 'invocation'. We then compute per-zone
607
- // concurrency as the number of unique `thread` seen per invocation.
688
+ // concurrency as the number of unique `global_idx` seen per invocation.
608
689
  bool IsRootRun() {
609
690
  // We are not the root if a Run was already active.
610
691
  return !run_active_.test_and_set(std::memory_order_acquire);
@@ -616,7 +697,7 @@ class Profiler {
616
697
  // when `PrintResults` is called.
617
698
  void EndRootRun() {
618
699
  UpdateResults();
619
- results_.CountThreadsAndReset();
700
+ results_.CountWorkersAndReset();
620
701
 
621
702
  run_active_.clear(std::memory_order_release);
622
703
  }
@@ -625,58 +706,59 @@ class Profiler {
625
706
  // zones. Resets all state, can be called again after more zones.
626
707
  void PrintResults() {
627
708
  UpdateResults();
628
- // `CountThreadsAndReset` is fused into `Print`, so do not call it here.
709
+ // `CountWorkersAndReset` is fused into `Print`, so do not call it here.
629
710
 
630
- results_.Print(names_);
631
- }
711
+ results_.PrintAndReset(zones_);
632
712
 
633
- // Only for use by Zone; called from any thread.
634
- profiler::PerThread& GetThread(size_t thread) {
635
- HWY_DASSERT(thread < profiler::kMaxThreads);
636
- return threads_[thread];
713
+ funcs_.CallAll();
637
714
  }
638
- profiler::Accumulators& Accumulators() { return accumulators_; }
715
+
716
+ // TODO: remove when no longer called.
717
+ void SetMaxThreads(size_t) {}
639
718
 
640
719
  private:
641
720
  // Sets main thread index, computes self-overhead, and checks timer support.
642
721
  Profiler();
643
722
 
644
- // Called from the main thread.
723
+ profiler::PerWorker& GetWorker(size_t global_idx) {
724
+ HWY_DASSERT(workers_reserved_.Get(global_idx));
725
+ return workers_[global_idx];
726
+ }
727
+
728
+ // Moves accumulators into Results. Called from the main thread.
645
729
  void UpdateResults() {
646
- for (size_t thread = 0; thread < max_threads_; ++thread) {
647
- threads_[thread].MoveTo(thread, accumulators_, results_);
648
- }
730
+ // Ensure we see all writes from before the workers' release fence.
731
+ std::atomic_thread_fence(std::memory_order_acquire);
649
732
 
650
- // Check that all other threads did not have any zones.
651
- HWY_IF_CONSTEXPR(HWY_IS_DEBUG_BUILD) {
652
- for (size_t thread = max_threads_; thread < profiler::kMaxThreads;
653
- ++thread) {
654
- HWY_ASSERT(!threads_[thread].HadAnyZones());
655
- }
656
- }
733
+ workers_reserved_.Foreach([&](size_t global_idx) {
734
+ workers_[global_idx].MoveTo(global_idx, accumulators_, results_);
735
+ });
657
736
  }
658
737
 
659
- static thread_local size_t s_thread;
660
- static std::atomic<size_t> s_num_threads;
661
- size_t max_threads_ = profiler::kMaxThreads;
738
+ static thread_local size_t s_global_idx;
739
+
740
+ // These are atomic because `ThreadFunc` reserves its slot(s) and even
741
+ // `ThreadPool::ThreadPool` may be called concurrently. Both have bit `i` set
742
+ // between calls to `Reserve*(i)` and `Free*(i)`. They are consulted in
743
+ // `UpdateResults` and to validate arguments in debug builds, and only updated
744
+ // in the pool/thread init/shutdown.
745
+ profiler::AtomicWorkerSet workers_reserved_;
662
746
 
663
747
  std::atomic_flag run_active_ = ATOMIC_FLAG_INIT;
664
748
 
665
- // To avoid locking, each thread has its own working set. We could access this
749
+ profiler::Funcs funcs_;
750
+
751
+ // To avoid locking, each worker has its own working set. We could access this
666
752
  // through `thread_local` pointers, but that is slow to read on x86. Because
667
- // our `ThreadPool` anyway passes a `thread` argument, we can instead pass
753
+ // our `ThreadPool` anyway passes a `global_idx` argument, we can instead pass
668
754
  // that through the `PROFILER_ZONE2/PROFILER_ZONE3` macros.
669
- profiler::PerThread threads_[profiler::kMaxThreads];
755
+ profiler::PerWorker workers_[profiler::kMaxWorkers];
670
756
 
671
757
  profiler::Accumulators accumulators_;
672
758
 
673
- // Updated by the main thread after the root `ThreadPool::Run` and during
674
- // `PrintResults`.
675
- profiler::ConcurrencyStats concurrency_[profiler::kMaxZones];
676
-
677
- profiler::Names names_;
678
-
679
759
  profiler::Results results_;
760
+
761
+ profiler::Zones zones_;
680
762
  };
681
763
 
682
764
  namespace profiler {
@@ -684,35 +766,33 @@ namespace profiler {
684
766
  // RAII for zone entry/exit.
685
767
  class Zone {
686
768
  public:
687
- // Thread-compatible; must not be called concurrently with the same `thread`.
688
- // `thread` must be < `HWY_MIN(kMaxThreads, max_threads_)`, and is typically:
689
- // - passed from `ThreadPool` via `PROFILER_ZONE2/PROFILER_ZONE3`. NOTE:
690
- // this value must be unique across all pools, which requires an offset to
691
- // a nested pool's `thread` argument.
692
- // - obtained from `Profiler::Thread()`, or
693
- // - 0 if only a single thread is active.
694
- Zone(Profiler& profiler, size_t thread, ZoneHandle zone)
769
+ // Thread-compatible; must not call concurrently with the same `global_idx`,
770
+ // which is either:
771
+ // - passed from `ThreadPool::Run` (if it was constructed with non-default
772
+ // `PoolWorkerMapping`) to `PROFILER_ZONE2/PROFILER_ZONE3`;
773
+ // - obtained from `Profiler::GlobalIdx()`; or
774
+ // - 0 if running on the main thread.
775
+ Zone(Profiler& profiler, size_t global_idx, ZoneHandle zone)
695
776
  : profiler_(profiler) {
696
777
  HWY_FENCE;
697
778
  const uint64_t t_enter = timer::Start();
698
779
  HWY_FENCE;
699
- thread_ = static_cast<uint32_t>(thread);
780
+ global_idx_ = static_cast<uint32_t>(global_idx);
700
781
  zone_ = zone;
701
- profiler.GetThread(thread).Enter(t_enter);
782
+ profiler.Enter(t_enter, global_idx);
702
783
  HWY_FENCE;
703
784
  }
704
785
 
705
786
  ~Zone() {
706
787
  HWY_FENCE;
707
788
  const uint64_t t_exit = timer::Stop();
708
- profiler_.GetThread(thread_).Exit(t_exit, thread_, zone_,
709
- profiler_.Accumulators());
789
+ profiler_.Exit(t_exit, static_cast<size_t>(global_idx_), zone_);
710
790
  HWY_FENCE;
711
791
  }
712
792
 
713
793
  private:
714
794
  Profiler& profiler_;
715
- uint32_t thread_;
795
+ uint32_t global_idx_;
716
796
  ZoneHandle zone_;
717
797
  };
718
798
 
@@ -726,9 +806,15 @@ struct ZoneHandle {};
726
806
  struct Profiler {
727
807
  static HWY_DLLEXPORT Profiler& Get();
728
808
 
729
- static void InitThread() {}
809
+ // DEPRECATED: use `GlobalIdx` instead.
730
810
  static size_t Thread() { return 0; }
731
- void SetMaxThreads(size_t) {}
811
+ static size_t GlobalIdx() { return 0; }
812
+ static void SetGlobalIdx(size_t) {}
813
+ void ReserveWorker(size_t) {}
814
+ void FreeWorker(size_t) {}
815
+ void Enter(uint64_t, size_t) {}
816
+ void Exit(uint64_t, size_t, profiler::ZoneHandle) {}
817
+ uint64_t GetFirstDurationAndReset(size_t) { return 0; }
732
818
 
733
819
  const char* Name(profiler::ZoneHandle) const { return nullptr; }
734
820
  profiler::ZoneHandle AddZone(const char*,
@@ -736,10 +822,15 @@ struct Profiler {
736
822
  return profiler::ZoneHandle();
737
823
  }
738
824
 
825
+ void AddFunc(void*, ProfilerFunc) {}
826
+ void RemoveFunc(void*) {}
827
+
739
828
  bool IsRootRun() { return false; }
740
829
  void EndRootRun() {}
741
-
742
830
  void PrintResults() {}
831
+
832
+ // TODO: remove when no longer called.
833
+ void SetMaxThreads(size_t) {}
743
834
  };
744
835
 
745
836
  namespace profiler {
@@ -754,26 +845,26 @@ struct Zone {
754
845
 
755
846
  // Creates a `Zone` lvalue with a line-dependent name, which records the elapsed
756
847
  // time from here until the end of the current scope. `p` is from
757
- // `Profiler::Get()` or a cached reference. `thread` is < `kMaxThreads`. `zone`
848
+ // `Profiler::Get()` or a cached reference. `global_idx < kMaxWorkers`. `zone`
758
849
  // is the return value of `AddZone`. Separating its static init from the `Zone`
759
850
  // may be more efficient than `PROFILER_ZONE2`.
760
- #define PROFILER_ZONE3(p, thread, zone) \
761
- HWY_FENCE; \
762
- const hwy::profiler::Zone HWY_CONCAT(Z, __LINE__)(p, thread, zone); \
851
+ #define PROFILER_ZONE3(p, global_idx, zone) \
852
+ HWY_FENCE; \
853
+ const hwy::profiler::Zone HWY_CONCAT(Z, __LINE__)(p, global_idx, zone); \
763
854
  HWY_FENCE
764
855
 
765
856
  // For compatibility with old callers that do not pass `p` nor `flags`.
766
- // Also calls AddZone. Usage: `PROFILER_ZONE2(thread, "MyZone");`
767
- #define PROFILER_ZONE2(thread, name) \
857
+ // Also calls AddZone. Usage: `PROFILER_ZONE2(global_idx, "MyZone");`
858
+ #define PROFILER_ZONE2(global_idx, name) \
768
859
  static const hwy::profiler::ZoneHandle HWY_CONCAT(zone, __LINE__) = \
769
860
  hwy::Profiler::Get().AddZone(name); \
770
- PROFILER_ZONE3(hwy::Profiler::Get(), thread, HWY_CONCAT(zone, __LINE__))
771
- #define PROFILER_FUNC2(thread) PROFILER_ZONE2(thread, __func__)
861
+ PROFILER_ZONE3(hwy::Profiler::Get(), global_idx, HWY_CONCAT(zone, __LINE__))
862
+ #define PROFILER_FUNC2(global_idx) PROFILER_ZONE2(global_idx, __func__)
772
863
 
773
- // OBSOLETE: it is more efficient to pass `thread` from `ThreadPool` to
864
+ // OBSOLETE: it is more efficient to pass `global_idx` from `ThreadPool` to
774
865
  // `PROFILER_ZONE2/PROFILER_ZONE3`. Here we get it from thread_local storage.
775
- #define PROFILER_ZONE(name) PROFILER_ZONE2(hwy::Profiler::Thread(), name)
776
- #define PROFILER_FUNC PROFILER_FUNC2(hwy::Profiler::Thread())
866
+ #define PROFILER_ZONE(name) PROFILER_ZONE2(hwy::Profiler::GlobalIdx(), name)
867
+ #define PROFILER_FUNC PROFILER_FUNC2(hwy::Profiler::GlobalIdx())
777
868
 
778
869
  // DEPRECATED: Use `hwy::Profiler::Get()` directly instead.
779
870
  #define PROFILER_ADD_ZONE(name) hwy::Profiler::Get().AddZone(name)