@react-perfscope/core 0.7.0 → 1.0.0

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.
package/dist/index.cjs CHANGED
@@ -129,8 +129,14 @@ var import_trace_mapping = require("@jridgewell/trace-mapping");
129
129
 
130
130
  // src/self-requests.ts
131
131
  var selfRequests = /* @__PURE__ */ new Set();
132
+ var MAX_SELF_REQUESTS = 2e3;
132
133
  function markSelfRequest(url) {
134
+ selfRequests.delete(url);
133
135
  selfRequests.add(url);
136
+ if (selfRequests.size > MAX_SELF_REQUESTS) {
137
+ const oldest = selfRequests.values().next().value;
138
+ if (oldest !== void 0) selfRequests.delete(oldest);
139
+ }
134
140
  }
135
141
  function isSelfRequest(url) {
136
142
  return selfRequests.has(url);
@@ -139,23 +145,25 @@ function isSelfRequest(url) {
139
145
  // src/sourcemap.ts
140
146
  var CHROME_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
141
147
  var FIREFOX_FRAME = /^(.*?)@(.+?):(\d+):(\d+)$/;
148
+ function lookupFrame(tracer, frame) {
149
+ const pos = (0, import_trace_mapping.originalPositionFor)(tracer, { line: frame.line, column: frame.col - 1 });
150
+ if (pos.source == null || pos.line == null || pos.column == null) {
151
+ return frame;
152
+ }
153
+ const resolved = {
154
+ file: pos.source,
155
+ line: pos.line,
156
+ col: pos.column + 1
157
+ };
158
+ if (pos.name) resolved.fnName = pos.name;
159
+ else if (frame.fnName) resolved.fnName = frame.fnName;
160
+ return resolved;
161
+ }
142
162
  async function resolveFrame(frame, fetchMap) {
143
163
  try {
144
164
  const map = await fetchMap(frame.file);
145
165
  if (!map) return frame;
146
- const tracer = new import_trace_mapping.TraceMap(map);
147
- const pos = (0, import_trace_mapping.originalPositionFor)(tracer, { line: frame.line, column: frame.col });
148
- if (pos.source == null || pos.line == null || pos.column == null) {
149
- return frame;
150
- }
151
- const resolved = {
152
- file: pos.source,
153
- line: pos.line,
154
- col: pos.column
155
- };
156
- if (pos.name) resolved.fnName = pos.name;
157
- else if (frame.fnName) resolved.fnName = frame.fnName;
158
- return resolved;
166
+ return lookupFrame(new import_trace_mapping.TraceMap(map), frame);
159
167
  } catch (err) {
160
168
  console.warn("[react-perfscope] resolveFrame failed:", err);
161
169
  return frame;
@@ -175,13 +183,25 @@ function attachLazyStack(target, raw, skipTopFrames = 0) {
175
183
  }
176
184
  });
177
185
  }
186
+ var MAX_CACHED_MAPS = 50;
187
+ function sourceCacheKey(url) {
188
+ const q = url.indexOf("?");
189
+ const h = url.indexOf("#");
190
+ const end = Math.min(q === -1 ? url.length : q, h === -1 ? url.length : h);
191
+ return url.slice(0, end);
192
+ }
178
193
  function createSourceMapResolver(opts = {}) {
179
194
  const f = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
180
195
  const cache = /* @__PURE__ */ new Map();
181
- function fetchMapFor(sourceUrl) {
196
+ function fetchTracerFor(sourceUrl) {
182
197
  if (!f) return Promise.resolve(null);
183
- const existing = cache.get(sourceUrl);
184
- if (existing) return existing;
198
+ const key = sourceCacheKey(sourceUrl);
199
+ const existing = cache.get(key);
200
+ if (existing) {
201
+ cache.delete(key);
202
+ cache.set(key, existing);
203
+ return existing;
204
+ }
185
205
  const promise = (async () => {
186
206
  try {
187
207
  markSelfRequest(sourceUrl);
@@ -197,24 +217,31 @@ function createSourceMapResolver(opts = {}) {
197
217
  const header = ref.slice(0, commaIdx);
198
218
  const payload = ref.slice(commaIdx + 1);
199
219
  const decoded = header.includes("base64") ? typeof atob === "function" ? atob(payload) : globalThis.Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
200
- return JSON.parse(decoded);
220
+ return new import_trace_mapping.TraceMap(JSON.parse(decoded));
201
221
  }
202
222
  const mapUrl = new URL(ref, sourceUrl).href;
203
223
  markSelfRequest(mapUrl);
204
224
  const mapRes = await f(mapUrl);
205
225
  if (!mapRes || !mapRes.ok) return null;
206
- return await mapRes.json();
226
+ return new import_trace_mapping.TraceMap(await mapRes.json());
207
227
  } catch {
228
+ cache.delete(key);
208
229
  return null;
209
230
  }
210
231
  })();
211
- cache.set(sourceUrl, promise);
232
+ cache.set(key, promise);
233
+ if (cache.size > MAX_CACHED_MAPS) {
234
+ const oldest = cache.keys().next().value;
235
+ if (oldest !== void 0) cache.delete(oldest);
236
+ }
212
237
  return promise;
213
238
  }
214
239
  return {
215
240
  async resolve(frame) {
216
241
  try {
217
- return await resolveFrame(frame, fetchMapFor);
242
+ const tracer = await fetchTracerFor(frame.file);
243
+ if (!tracer) return frame;
244
+ return lookupFrame(tracer, frame);
218
245
  } catch {
219
246
  return frame;
220
247
  }
@@ -405,6 +432,7 @@ function createLongTasksCollector() {
405
432
  return {
406
433
  kind: "long-task",
407
434
  activate(emit) {
435
+ if (active) return;
408
436
  if (typeof PerformanceObserver === "undefined") {
409
437
  console.warn("[react-perfscope] PerformanceObserver not supported; long-tasks disabled");
410
438
  return;
@@ -456,6 +484,20 @@ function createLongTasksCollector() {
456
484
  };
457
485
  }
458
486
 
487
+ // src/self-measurement.ts
488
+ var depth = 0;
489
+ function withSelfMeasurement(fn) {
490
+ depth++;
491
+ try {
492
+ return fn();
493
+ } finally {
494
+ depth--;
495
+ }
496
+ }
497
+ function inSelfMeasurement() {
498
+ return depth > 0;
499
+ }
500
+
459
501
  // src/collectors/forced-reflow.ts
460
502
  var LAYOUT_GETTERS = [
461
503
  "offsetWidth",
@@ -518,7 +560,7 @@ function createForcedReflowCollector() {
518
560
  Object.defineProperty(proto, key, {
519
561
  configurable: true,
520
562
  get() {
521
- if (active) {
563
+ if (active && !inSelfMeasurement()) {
522
564
  if (!consumePendingMutations()) {
523
565
  return originalGet.call(this);
524
566
  }
@@ -543,7 +585,7 @@ function createForcedReflowCollector() {
543
585
  configurable: true,
544
586
  writable: true,
545
587
  value: function patchedLayoutMethod(...args) {
546
- if (active) {
588
+ if (active && !inSelfMeasurement()) {
547
589
  if (!consumePendingMutations()) {
548
590
  return original.apply(this, args);
549
591
  }
@@ -633,7 +675,7 @@ function collectPerfscopeRects() {
633
675
  function entryIsOnlyFromPerfscope(sources) {
634
676
  if (sources.length === 0) return false;
635
677
  if (typeof document === "undefined") return false;
636
- const hostRects = collectPerfscopeRects();
678
+ const hostRects = withSelfMeasurement(collectPerfscopeRects);
637
679
  if (hostRects.length === 0) return false;
638
680
  for (const src of sources) {
639
681
  let matched = false;
@@ -670,6 +712,7 @@ function createLayoutShiftCollector() {
670
712
  return {
671
713
  kind: "layout-shift",
672
714
  activate(emit) {
715
+ if (active) return;
673
716
  if (typeof PerformanceObserver === "undefined") {
674
717
  console.warn("[react-perfscope] PerformanceObserver not supported; layout-shift disabled");
675
718
  return;
@@ -728,6 +771,7 @@ function createNetworkCollector() {
728
771
  return {
729
772
  kind: "network",
730
773
  activate(emit) {
774
+ if (active) return;
731
775
  if (typeof PerformanceObserver === "undefined") {
732
776
  console.warn("[react-perfscope] PerformanceObserver not supported; network disabled");
733
777
  return;
@@ -771,37 +815,40 @@ function createNetworkCollector() {
771
815
 
772
816
  // src/collectors/web-vitals.ts
773
817
  var import_web_vitals = require("web-vitals");
818
+ var subscribed = false;
819
+ var sinks = /* @__PURE__ */ new Set();
820
+ function ensureSubscribed() {
821
+ if (subscribed) return true;
822
+ try {
823
+ const fan = (name) => (metric) => {
824
+ for (const sink of sinks) sink(name, metric.value);
825
+ };
826
+ (0, import_web_vitals.onLCP)(fan("LCP"));
827
+ (0, import_web_vitals.onINP)(fan("INP"));
828
+ (0, import_web_vitals.onCLS)(fan("CLS"));
829
+ (0, import_web_vitals.onFCP)(fan("FCP"));
830
+ (0, import_web_vitals.onTTFB)(fan("TTFB"));
831
+ subscribed = true;
832
+ } catch (err) {
833
+ console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
834
+ }
835
+ return subscribed;
836
+ }
774
837
  function createWebVitalsCollector() {
775
- let active = false;
776
- let subscribed = false;
777
838
  let emit = () => {
778
839
  };
779
- function makeHandler(name) {
780
- return (metric) => {
781
- if (!active) return;
782
- emit({ kind: "web-vital", name, value: metric.value });
783
- };
784
- }
840
+ const sink = (name, value) => {
841
+ emit({ kind: "web-vital", name, value });
842
+ };
785
843
  return {
786
844
  kind: "web-vital",
787
845
  activate(emitFn) {
846
+ if (!ensureSubscribed()) return;
788
847
  emit = emitFn;
789
- active = true;
790
- if (subscribed) return;
791
- try {
792
- (0, import_web_vitals.onLCP)(makeHandler("LCP"));
793
- (0, import_web_vitals.onINP)(makeHandler("INP"));
794
- (0, import_web_vitals.onCLS)(makeHandler("CLS"));
795
- (0, import_web_vitals.onFCP)(makeHandler("FCP"));
796
- (0, import_web_vitals.onTTFB)(makeHandler("TTFB"));
797
- subscribed = true;
798
- } catch (err) {
799
- console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
800
- active = false;
801
- }
848
+ sinks.add(sink);
802
849
  },
803
850
  deactivate() {
804
- active = false;
851
+ sinks.delete(sink);
805
852
  }
806
853
  };
807
854
  }
@@ -874,6 +921,7 @@ function createHeapCollector() {
874
921
  return {
875
922
  kind: "heap",
876
923
  activate() {
924
+ if (timer != null) return;
877
925
  samples = [];
878
926
  if (!readMemory()) return;
879
927
  sample();
@@ -894,6 +942,7 @@ function createHeapCollector() {
894
942
 
895
943
  // src/collectors/interaction.ts
896
944
  var DURATION_THRESHOLD = 40;
945
+ var MAX_ENTRIES = 5e3;
897
946
  function selectorFor(target) {
898
947
  const el = target;
899
948
  if (!el || typeof el.tagName !== "string") return void 0;
@@ -916,13 +965,17 @@ function createInteractionCollector() {
916
965
  return {
917
966
  kind: "interaction",
918
967
  activate() {
968
+ if (active) return;
919
969
  if (typeof PerformanceObserver === "undefined" || !supportsEventTiming()) return;
920
970
  active = true;
921
971
  entries = [];
922
972
  try {
923
973
  observer = new PerformanceObserver((list) => {
924
974
  if (!active) return;
925
- for (const e of list.getEntries()) entries.push(e);
975
+ for (const e of list.getEntries()) {
976
+ if (entries.length >= MAX_ENTRIES) break;
977
+ entries.push(e);
978
+ }
926
979
  });
927
980
  observer.observe({ type: "event", buffered: false, durationThreshold: DURATION_THRESHOLD });
928
981
  try {
@@ -946,8 +999,10 @@ function createInteractionCollector() {
946
999
  }
947
1000
  },
948
1001
  finalize(result) {
1002
+ const buffered = entries;
1003
+ entries = [];
949
1004
  const byId = /* @__PURE__ */ new Map();
950
- for (const e of entries) {
1005
+ for (const e of buffered) {
951
1006
  const id = e.interactionId;
952
1007
  if (!id) continue;
953
1008
  const group = byId.get(id);
@@ -1146,6 +1201,7 @@ function createSelfProfilingCollector() {
1146
1201
  return {
1147
1202
  kind: "long-task",
1148
1203
  activate() {
1204
+ if (profiler) return;
1149
1205
  tracePromise = null;
1150
1206
  const Ctor = globalThis.Profiler;
1151
1207
  if (typeof Ctor !== "function") return;
@@ -1163,6 +1219,8 @@ function createSelfProfilingCollector() {
1163
1219
  if (!profiler) return;
1164
1220
  try {
1165
1221
  tracePromise = profiler.stop();
1222
+ tracePromise.catch(() => {
1223
+ });
1166
1224
  } catch (err) {
1167
1225
  console.warn("[react-perfscope] self-profiling stop failed:", err);
1168
1226
  tracePromise = null;