Skip to content

Commit 707a863

Browse files
rubennortefacebook-github-bot
authored andcommitted
Implement maxDuration option for PerformanceTracer (#52935)
Summary: Pull Request resolved: #52935 Changelog: [internal] This implements a new option for `PerformanceTracer::startTracing` to specify a maximum duration for the recording, keeping only the last record events. This will allow us to implement an "always-on" profiling mode for RN with a low overhead. In the future, we can extend this with a `maxBufferSize` option. See the comment in `PerformanceTracer.cpp` for implementation details. Reviewed By: hoxyq Differential Revision: D79340014 fbshipit-source-id: 3260724a775a574fe5e52d47358a3a5abd0d2ee7
1 parent 30e2b35 commit 707a863

3 files changed

Lines changed: 157 additions & 4 deletions

File tree

packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.cpp

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818

1919
namespace facebook::react::jsinspector_modern::tracing {
2020

21+
namespace {
22+
/**
23+
* Instead of validating that the indicated duration is positive, we can ensure
24+
* the value is sensible (less than 1 seccond doesn't make much sense).
25+
*/
26+
const HighResDuration MIN_VALUE_FOR_MAX_TRACE_DURATION_OPTION =
27+
HighResDuration::fromMilliseconds(1000); // 1 second
28+
} // namespace
29+
2130
PerformanceTracer& PerformanceTracer::getInstance() {
2231
static PerformanceTracer tracer;
2332
return tracer;
@@ -27,20 +36,38 @@ PerformanceTracer::PerformanceTracer()
2736
: processId_(oscompat::getCurrentProcessId()) {}
2837

2938
bool PerformanceTracer::startTracing() {
39+
return startTracingImpl();
40+
}
41+
42+
bool PerformanceTracer::startTracing(HighResDuration maxDuration) {
43+
return startTracingImpl(maxDuration);
44+
}
45+
46+
bool PerformanceTracer::startTracingImpl(
47+
std::optional<HighResDuration> maxDuration) {
3048
std::lock_guard lock(mutex_);
3149

3250
if (tracingAtomic_) {
3351
return false;
3452
}
3553

3654
tracingAtomic_ = true;
55+
56+
if (maxDuration && *maxDuration < MIN_VALUE_FOR_MAX_TRACE_DURATION_OPTION) {
57+
throw std::invalid_argument("maxDuration should be at least 1 second");
58+
}
59+
3760
currentTraceStartTime_ = HighResTimeStamp::now();
61+
currentTraceMaxDuration_ = maxDuration;
62+
3863
return true;
3964
}
4065

4166
std::optional<std::vector<TraceEvent>> PerformanceTracer::stopTracing() {
4267
std::vector<TraceEvent> events;
4368

69+
auto currentTraceEndTime = HighResTimeStamp::now();
70+
4471
{
4572
std::lock_guard lock(mutex_);
4673

@@ -50,8 +77,15 @@ std::optional<std::vector<TraceEvent>> PerformanceTracer::stopTracing() {
5077

5178
tracingAtomic_ = false;
5279
performanceMeasureCount_ = 0;
80+
currentTraceMaxDuration_ = std::nullopt;
5381

54-
buffer_.swap(events);
82+
events = collectEventsAndClearBuffers(currentTraceEndTime);
83+
}
84+
85+
auto currentTraceStartTime = currentTraceStartTime_;
86+
if (currentTraceMaxDuration_ &&
87+
currentTraceEndTime - *currentTraceMaxDuration_ > currentTraceStartTime) {
88+
currentTraceStartTime = currentTraceEndTime - *currentTraceMaxDuration_;
5589
}
5690

5791
// This is synthetic Trace Event, which should not be represented on a
@@ -65,7 +99,7 @@ std::optional<std::vector<TraceEvent>> PerformanceTracer::stopTracing() {
6599
.name = "TracingStartedInPage",
66100
.cat = "disabled-by-default-devtools.timeline",
67101
.ph = 'I',
68-
.ts = currentTraceStartTime_,
102+
.ts = currentTraceStartTime,
69103
.pid = processId_,
70104
.tid = oscompat::getCurrentThreadId(),
71105
.args = folly::dynamic::object("data", folly::dynamic::object()),
@@ -75,7 +109,7 @@ std::optional<std::vector<TraceEvent>> PerformanceTracer::stopTracing() {
75109
.name = "ReactNative-TracingStopped",
76110
.cat = "disabled-by-default-devtools.timeline",
77111
.ph = 'I',
78-
.ts = HighResTimeStamp::now(),
112+
.ts = currentTraceEndTime,
79113
.pid = processId_,
80114
.tid = oscompat::getCurrentThreadId(),
81115
});
@@ -302,8 +336,96 @@ PerformanceTracer::constructRuntimeProfileChunkTraceEvent(
302336
};
303337
}
304338

339+
#pragma mark - Tracing window methods
340+
341+
/**
342+
* If a `maxDuration` value is set when starting a trace, we use 2 buffers
343+
* for events. Each buffer can contain entries for a range up to `maxDuration`.
344+
* When the current buffer is full, we clear the previous one and we start
345+
* collecting events in a new buffer, which becomes current.
346+
*
347+
* Example:
348+
* - Start:
349+
* previousBuffer: null, currentBuffer: []
350+
* - As entries are added:
351+
* previousBuffer: null, currentBuffer: [a, b, c...]
352+
* - When now - currentBuffer start time > maxDuration:
353+
* previousBuffer: [a, b, c...], currentBuffer: []
354+
* - As entries are added:
355+
* previousbuffer: [a, b, c...], currentBuffer: [x, y, z...]
356+
* - When now - currentBuffer start time > maxDuration:
357+
* previousBuffer: [x, y, z...], currentBuffer: []
358+
* - When the trace finishes:
359+
* We collect all events in both buffers that are still within
360+
* `maxDuration`.
361+
*
362+
* This way, we ensure we keep all events in the `maxDuration` window, and
363+
* clearing expired events is trivial (just clearing a vector).
364+
*/
365+
366+
std::vector<TraceEvent> PerformanceTracer::collectEventsAndClearBuffers(
367+
HighResTimeStamp currentTraceEndTime) {
368+
std::vector<TraceEvent> events;
369+
370+
if (currentTraceMaxDuration_) {
371+
// Collect non-expired entries from the previous buffer
372+
if (previousBuffer_ != nullptr) {
373+
collectEventsAndClearBuffer(
374+
events, *previousBuffer_, currentTraceEndTime);
375+
}
376+
377+
collectEventsAndClearBuffer(events, *currentBuffer_, currentTraceEndTime);
378+
379+
// Reset state.
380+
currentBuffer_ = &buffer_;
381+
previousBuffer_ = nullptr;
382+
} else {
383+
// Trivial case.
384+
currentBuffer_->swap(events);
385+
}
386+
387+
return events;
388+
}
389+
390+
void PerformanceTracer::collectEventsAndClearBuffer(
391+
std::vector<TraceEvent>& events,
392+
std::vector<TraceEvent>& buffer,
393+
HighResTimeStamp currentTraceEndTime) {
394+
for (auto&& event : buffer) {
395+
if (isInTracingWindow(currentTraceEndTime, event.createdAt_)) {
396+
events.emplace_back(std::move(event));
397+
}
398+
}
399+
400+
buffer.clear();
401+
buffer.shrink_to_fit();
402+
}
403+
404+
bool PerformanceTracer::isInTracingWindow(
405+
HighResTimeStamp now,
406+
HighResTimeStamp timeStampToCheck) const {
407+
if (!currentTraceMaxDuration_) {
408+
return true;
409+
}
410+
411+
return timeStampToCheck > now - *currentTraceMaxDuration_;
412+
}
413+
305414
void PerformanceTracer::enqueueEvent(TraceEvent&& traceEvent) {
306-
buffer_.emplace_back(std::move(traceEvent));
415+
if (currentTraceMaxDuration_) {
416+
// Check if the current buffer is "full"
417+
if (traceEvent.createdAt_ >
418+
currentBufferStartTime_ + *currentTraceMaxDuration_) {
419+
// We moved past the current buffer. We need to switch the other buffer as
420+
// current.
421+
previousBuffer_ = currentBuffer_;
422+
currentBuffer_ = currentBuffer_ == &buffer_ ? &altBuffer_ : &buffer_;
423+
currentBuffer_->clear();
424+
currentBufferStartTime_ = traceEvent.createdAt_;
425+
}
426+
}
427+
428+
currentBuffer_->emplace_back(std::move(traceEvent));
307429
}
308430

309431
} // namespace facebook::react::jsinspector_modern::tracing

packages/react-native/ReactCommon/jsinspector-modern/tracing/PerformanceTracer.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ class PerformanceTracer {
3737
*/
3838
bool startTracing();
3939

40+
/**
41+
* Starts a tracing session with a maximum duration (events older than this
42+
* duration are dropped). Returns `false` if already tracing.
43+
*/
44+
bool startTracing(HighResDuration maxDuration);
45+
4046
/**
4147
* If there is a current tracing session, it stops tracing and returns all
4248
* collected events. Otherwise, it returns empty.
@@ -151,14 +157,34 @@ class PerformanceTracer {
151157

152158
HighResTimeStamp currentTraceStartTime_;
153159

160+
std::optional<HighResDuration> currentTraceMaxDuration_;
161+
154162
std::vector<TraceEvent> buffer_;
155163

164+
// These fields are only used when setting a max duration on the trace.
165+
std::vector<TraceEvent> altBuffer_;
166+
std::vector<TraceEvent>* currentBuffer_ = &buffer_;
167+
std::vector<TraceEvent>* previousBuffer_{};
168+
HighResTimeStamp currentBufferStartTime_;
169+
156170
/**
157171
* Protects data members of this class for concurrent access, including
158172
* the tracingAtomic_, in order to eliminate potential "logic" races.
159173
*/
160174
std::mutex mutex_;
161175

176+
bool startTracingImpl(
177+
std::optional<HighResDuration> maxDuration = std::nullopt);
178+
179+
std::vector<TraceEvent> collectEventsAndClearBuffers(
180+
HighResTimeStamp currentTraceEndTime);
181+
void collectEventsAndClearBuffer(
182+
std::vector<TraceEvent>& events,
183+
std::vector<TraceEvent>& buffer,
184+
HighResTimeStamp currentTraceEndTime);
185+
bool isInTracingWindow(
186+
HighResTimeStamp now,
187+
HighResTimeStamp timeStampToCheck) const;
162188
void enqueueEvent(TraceEvent&& event);
163189
};
164190

packages/react-native/ReactCommon/jsinspector-modern/tracing/TraceEvent.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ struct TraceEvent {
6666
* complete events ("ph": "X").
6767
*/
6868
std::optional<HighResDuration> dur;
69+
70+
/**
71+
* This is not part of the Trace Event Format.
72+
*/
73+
HighResTimeStamp createdAt_ = HighResTimeStamp::now();
6974
};
7075

7176
} // namespace facebook::react::jsinspector_modern::tracing

0 commit comments

Comments
 (0)