@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.js CHANGED
@@ -78,8 +78,14 @@ import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
78
78
 
79
79
  // src/self-requests.ts
80
80
  var selfRequests = /* @__PURE__ */ new Set();
81
+ var MAX_SELF_REQUESTS = 2e3;
81
82
  function markSelfRequest(url) {
83
+ selfRequests.delete(url);
82
84
  selfRequests.add(url);
85
+ if (selfRequests.size > MAX_SELF_REQUESTS) {
86
+ const oldest = selfRequests.values().next().value;
87
+ if (oldest !== void 0) selfRequests.delete(oldest);
88
+ }
83
89
  }
84
90
  function isSelfRequest(url) {
85
91
  return selfRequests.has(url);
@@ -88,23 +94,25 @@ function isSelfRequest(url) {
88
94
  // src/sourcemap.ts
89
95
  var CHROME_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
90
96
  var FIREFOX_FRAME = /^(.*?)@(.+?):(\d+):(\d+)$/;
97
+ function lookupFrame(tracer, frame) {
98
+ const pos = originalPositionFor(tracer, { line: frame.line, column: frame.col - 1 });
99
+ if (pos.source == null || pos.line == null || pos.column == null) {
100
+ return frame;
101
+ }
102
+ const resolved = {
103
+ file: pos.source,
104
+ line: pos.line,
105
+ col: pos.column + 1
106
+ };
107
+ if (pos.name) resolved.fnName = pos.name;
108
+ else if (frame.fnName) resolved.fnName = frame.fnName;
109
+ return resolved;
110
+ }
91
111
  async function resolveFrame(frame, fetchMap) {
92
112
  try {
93
113
  const map = await fetchMap(frame.file);
94
114
  if (!map) return frame;
95
- const tracer = new TraceMap(map);
96
- const pos = originalPositionFor(tracer, { line: frame.line, column: frame.col });
97
- if (pos.source == null || pos.line == null || pos.column == null) {
98
- return frame;
99
- }
100
- const resolved = {
101
- file: pos.source,
102
- line: pos.line,
103
- col: pos.column
104
- };
105
- if (pos.name) resolved.fnName = pos.name;
106
- else if (frame.fnName) resolved.fnName = frame.fnName;
107
- return resolved;
115
+ return lookupFrame(new TraceMap(map), frame);
108
116
  } catch (err) {
109
117
  console.warn("[react-perfscope] resolveFrame failed:", err);
110
118
  return frame;
@@ -124,13 +132,25 @@ function attachLazyStack(target, raw, skipTopFrames = 0) {
124
132
  }
125
133
  });
126
134
  }
135
+ var MAX_CACHED_MAPS = 50;
136
+ function sourceCacheKey(url) {
137
+ const q = url.indexOf("?");
138
+ const h = url.indexOf("#");
139
+ const end = Math.min(q === -1 ? url.length : q, h === -1 ? url.length : h);
140
+ return url.slice(0, end);
141
+ }
127
142
  function createSourceMapResolver(opts = {}) {
128
143
  const f = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
129
144
  const cache = /* @__PURE__ */ new Map();
130
- function fetchMapFor(sourceUrl) {
145
+ function fetchTracerFor(sourceUrl) {
131
146
  if (!f) return Promise.resolve(null);
132
- const existing = cache.get(sourceUrl);
133
- if (existing) return existing;
147
+ const key = sourceCacheKey(sourceUrl);
148
+ const existing = cache.get(key);
149
+ if (existing) {
150
+ cache.delete(key);
151
+ cache.set(key, existing);
152
+ return existing;
153
+ }
134
154
  const promise = (async () => {
135
155
  try {
136
156
  markSelfRequest(sourceUrl);
@@ -146,24 +166,31 @@ function createSourceMapResolver(opts = {}) {
146
166
  const header = ref.slice(0, commaIdx);
147
167
  const payload = ref.slice(commaIdx + 1);
148
168
  const decoded = header.includes("base64") ? typeof atob === "function" ? atob(payload) : globalThis.Buffer.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
149
- return JSON.parse(decoded);
169
+ return new TraceMap(JSON.parse(decoded));
150
170
  }
151
171
  const mapUrl = new URL(ref, sourceUrl).href;
152
172
  markSelfRequest(mapUrl);
153
173
  const mapRes = await f(mapUrl);
154
174
  if (!mapRes || !mapRes.ok) return null;
155
- return await mapRes.json();
175
+ return new TraceMap(await mapRes.json());
156
176
  } catch {
177
+ cache.delete(key);
157
178
  return null;
158
179
  }
159
180
  })();
160
- cache.set(sourceUrl, promise);
181
+ cache.set(key, promise);
182
+ if (cache.size > MAX_CACHED_MAPS) {
183
+ const oldest = cache.keys().next().value;
184
+ if (oldest !== void 0) cache.delete(oldest);
185
+ }
161
186
  return promise;
162
187
  }
163
188
  return {
164
189
  async resolve(frame) {
165
190
  try {
166
- return await resolveFrame(frame, fetchMapFor);
191
+ const tracer = await fetchTracerFor(frame.file);
192
+ if (!tracer) return frame;
193
+ return lookupFrame(tracer, frame);
167
194
  } catch {
168
195
  return frame;
169
196
  }
@@ -354,6 +381,7 @@ function createLongTasksCollector() {
354
381
  return {
355
382
  kind: "long-task",
356
383
  activate(emit) {
384
+ if (active) return;
357
385
  if (typeof PerformanceObserver === "undefined") {
358
386
  console.warn("[react-perfscope] PerformanceObserver not supported; long-tasks disabled");
359
387
  return;
@@ -405,6 +433,20 @@ function createLongTasksCollector() {
405
433
  };
406
434
  }
407
435
 
436
+ // src/self-measurement.ts
437
+ var depth = 0;
438
+ function withSelfMeasurement(fn) {
439
+ depth++;
440
+ try {
441
+ return fn();
442
+ } finally {
443
+ depth--;
444
+ }
445
+ }
446
+ function inSelfMeasurement() {
447
+ return depth > 0;
448
+ }
449
+
408
450
  // src/collectors/forced-reflow.ts
409
451
  var LAYOUT_GETTERS = [
410
452
  "offsetWidth",
@@ -467,7 +509,7 @@ function createForcedReflowCollector() {
467
509
  Object.defineProperty(proto, key, {
468
510
  configurable: true,
469
511
  get() {
470
- if (active) {
512
+ if (active && !inSelfMeasurement()) {
471
513
  if (!consumePendingMutations()) {
472
514
  return originalGet.call(this);
473
515
  }
@@ -492,7 +534,7 @@ function createForcedReflowCollector() {
492
534
  configurable: true,
493
535
  writable: true,
494
536
  value: function patchedLayoutMethod(...args) {
495
- if (active) {
537
+ if (active && !inSelfMeasurement()) {
496
538
  if (!consumePendingMutations()) {
497
539
  return original.apply(this, args);
498
540
  }
@@ -582,7 +624,7 @@ function collectPerfscopeRects() {
582
624
  function entryIsOnlyFromPerfscope(sources) {
583
625
  if (sources.length === 0) return false;
584
626
  if (typeof document === "undefined") return false;
585
- const hostRects = collectPerfscopeRects();
627
+ const hostRects = withSelfMeasurement(collectPerfscopeRects);
586
628
  if (hostRects.length === 0) return false;
587
629
  for (const src of sources) {
588
630
  let matched = false;
@@ -619,6 +661,7 @@ function createLayoutShiftCollector() {
619
661
  return {
620
662
  kind: "layout-shift",
621
663
  activate(emit) {
664
+ if (active) return;
622
665
  if (typeof PerformanceObserver === "undefined") {
623
666
  console.warn("[react-perfscope] PerformanceObserver not supported; layout-shift disabled");
624
667
  return;
@@ -677,6 +720,7 @@ function createNetworkCollector() {
677
720
  return {
678
721
  kind: "network",
679
722
  activate(emit) {
723
+ if (active) return;
680
724
  if (typeof PerformanceObserver === "undefined") {
681
725
  console.warn("[react-perfscope] PerformanceObserver not supported; network disabled");
682
726
  return;
@@ -720,37 +764,40 @@ function createNetworkCollector() {
720
764
 
721
765
  // src/collectors/web-vitals.ts
722
766
  import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals";
767
+ var subscribed = false;
768
+ var sinks = /* @__PURE__ */ new Set();
769
+ function ensureSubscribed() {
770
+ if (subscribed) return true;
771
+ try {
772
+ const fan = (name) => (metric) => {
773
+ for (const sink of sinks) sink(name, metric.value);
774
+ };
775
+ onLCP(fan("LCP"));
776
+ onINP(fan("INP"));
777
+ onCLS(fan("CLS"));
778
+ onFCP(fan("FCP"));
779
+ onTTFB(fan("TTFB"));
780
+ subscribed = true;
781
+ } catch (err) {
782
+ console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
783
+ }
784
+ return subscribed;
785
+ }
723
786
  function createWebVitalsCollector() {
724
- let active = false;
725
- let subscribed = false;
726
787
  let emit = () => {
727
788
  };
728
- function makeHandler(name) {
729
- return (metric) => {
730
- if (!active) return;
731
- emit({ kind: "web-vital", name, value: metric.value });
732
- };
733
- }
789
+ const sink = (name, value) => {
790
+ emit({ kind: "web-vital", name, value });
791
+ };
734
792
  return {
735
793
  kind: "web-vital",
736
794
  activate(emitFn) {
795
+ if (!ensureSubscribed()) return;
737
796
  emit = emitFn;
738
- active = true;
739
- if (subscribed) return;
740
- try {
741
- onLCP(makeHandler("LCP"));
742
- onINP(makeHandler("INP"));
743
- onCLS(makeHandler("CLS"));
744
- onFCP(makeHandler("FCP"));
745
- onTTFB(makeHandler("TTFB"));
746
- subscribed = true;
747
- } catch (err) {
748
- console.warn("[react-perfscope] web-vitals collector failed to subscribe:", err);
749
- active = false;
750
- }
797
+ sinks.add(sink);
751
798
  },
752
799
  deactivate() {
753
- active = false;
800
+ sinks.delete(sink);
754
801
  }
755
802
  };
756
803
  }
@@ -823,6 +870,7 @@ function createHeapCollector() {
823
870
  return {
824
871
  kind: "heap",
825
872
  activate() {
873
+ if (timer != null) return;
826
874
  samples = [];
827
875
  if (!readMemory()) return;
828
876
  sample();
@@ -843,6 +891,7 @@ function createHeapCollector() {
843
891
 
844
892
  // src/collectors/interaction.ts
845
893
  var DURATION_THRESHOLD = 40;
894
+ var MAX_ENTRIES = 5e3;
846
895
  function selectorFor(target) {
847
896
  const el = target;
848
897
  if (!el || typeof el.tagName !== "string") return void 0;
@@ -865,13 +914,17 @@ function createInteractionCollector() {
865
914
  return {
866
915
  kind: "interaction",
867
916
  activate() {
917
+ if (active) return;
868
918
  if (typeof PerformanceObserver === "undefined" || !supportsEventTiming()) return;
869
919
  active = true;
870
920
  entries = [];
871
921
  try {
872
922
  observer = new PerformanceObserver((list) => {
873
923
  if (!active) return;
874
- for (const e of list.getEntries()) entries.push(e);
924
+ for (const e of list.getEntries()) {
925
+ if (entries.length >= MAX_ENTRIES) break;
926
+ entries.push(e);
927
+ }
875
928
  });
876
929
  observer.observe({ type: "event", buffered: false, durationThreshold: DURATION_THRESHOLD });
877
930
  try {
@@ -895,8 +948,10 @@ function createInteractionCollector() {
895
948
  }
896
949
  },
897
950
  finalize(result) {
951
+ const buffered = entries;
952
+ entries = [];
898
953
  const byId = /* @__PURE__ */ new Map();
899
- for (const e of entries) {
954
+ for (const e of buffered) {
900
955
  const id = e.interactionId;
901
956
  if (!id) continue;
902
957
  const group = byId.get(id);
@@ -1095,6 +1150,7 @@ function createSelfProfilingCollector() {
1095
1150
  return {
1096
1151
  kind: "long-task",
1097
1152
  activate() {
1153
+ if (profiler) return;
1098
1154
  tracePromise = null;
1099
1155
  const Ctor = globalThis.Profiler;
1100
1156
  if (typeof Ctor !== "function") return;
@@ -1112,6 +1168,8 @@ function createSelfProfilingCollector() {
1112
1168
  if (!profiler) return;
1113
1169
  try {
1114
1170
  tracePromise = profiler.stop();
1171
+ tracePromise.catch(() => {
1172
+ });
1115
1173
  } catch (err) {
1116
1174
  console.warn("[react-perfscope] self-profiling stop failed:", err);
1117
1175
  tracePromise = null;