@buoy-gg/benchmark 2.1.3 → 2.1.4-beta.1

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 (34) hide show
  1. package/lib/commonjs/benchmarking/BenchmarkComparator.js +221 -1
  2. package/lib/commonjs/benchmarking/BenchmarkRecorder.js +497 -1
  3. package/lib/commonjs/benchmarking/BenchmarkStorage.js +235 -1
  4. package/lib/commonjs/benchmarking/index.js +83 -1
  5. package/lib/commonjs/benchmarking/types.js +13 -1
  6. package/lib/commonjs/components/BenchmarkCompareView.js +475 -1
  7. package/lib/commonjs/components/BenchmarkDetailView.js +346 -1
  8. package/lib/commonjs/components/BenchmarkModal.js +505 -1
  9. package/lib/commonjs/components/BenchmarkSessionCard.js +193 -1
  10. package/lib/commonjs/index.js +62 -1
  11. package/lib/commonjs/preset.js +86 -1
  12. package/lib/module/benchmarking/BenchmarkComparator.js +216 -1
  13. package/lib/module/benchmarking/BenchmarkRecorder.js +493 -1
  14. package/lib/module/benchmarking/BenchmarkStorage.js +227 -1
  15. package/lib/module/benchmarking/index.js +48 -1
  16. package/lib/module/benchmarking/types.js +13 -1
  17. package/lib/module/components/BenchmarkCompareView.js +469 -1
  18. package/lib/module/components/BenchmarkDetailView.js +340 -1
  19. package/lib/module/components/BenchmarkModal.js +499 -1
  20. package/lib/module/components/BenchmarkSessionCard.js +187 -1
  21. package/lib/module/index.js +39 -1
  22. package/lib/module/preset.js +81 -1
  23. package/lib/typescript/benchmarking/BenchmarkComparator.d.ts.map +1 -0
  24. package/lib/typescript/benchmarking/BenchmarkRecorder.d.ts.map +1 -0
  25. package/lib/typescript/benchmarking/BenchmarkStorage.d.ts.map +1 -0
  26. package/lib/typescript/benchmarking/index.d.ts.map +1 -0
  27. package/lib/typescript/benchmarking/types.d.ts.map +1 -0
  28. package/lib/typescript/components/BenchmarkCompareView.d.ts.map +1 -0
  29. package/lib/typescript/components/BenchmarkDetailView.d.ts.map +1 -0
  30. package/lib/typescript/components/BenchmarkModal.d.ts.map +1 -0
  31. package/lib/typescript/components/BenchmarkSessionCard.d.ts.map +1 -0
  32. package/lib/typescript/index.d.ts.map +1 -0
  33. package/lib/typescript/preset.d.ts.map +1 -0
  34. package/package.json +2 -2
@@ -1 +1,499 @@
1
- "use strict";import React,{useState,useEffect,useCallback,useRef,useMemo}from"react";import{View,Text,TouchableOpacity,StyleSheet,FlatList,Alert}from"react-native";import{JsModal,ModalHeader,macOSColors,devToolsStorageKeys,Trash2,RefreshCw,GitBranch,Edit3,Copy,copyToClipboard,PowerToggleButton}from"@buoy-gg/shared-ui";import{benchmarkRecorder,BenchmarkStorage,BenchmarkComparator,createAsyncStorageAdapter}from"../benchmarking";import{BenchmarkSessionCard}from"./BenchmarkSessionCard";import{BenchmarkDetailView}from"./BenchmarkDetailView";import{BenchmarkCompareView}from"./BenchmarkCompareView";import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";const storageAdapter=createAsyncStorageAdapter(),storage=storageAdapter?new BenchmarkStorage(storageAdapter):null;export function BenchmarkModal({visible:e,onClose:t,onBack:o,onMinimize:a,enableSharedModalDimensions:s=!1}){const[r,n]=useState(!1),[i,l]=useState([]),[c,m]=useState(null),[d,p]=useState("list"),[u,g]=useState(!1),[h,y]=useState(new Set),[S,T]=useState(null),[C,x]=useState(0);useRef(!1),useEffect(()=>{e&&storage&&(async()=>{const e=await storage.listReports();l(e)})()},[e,C]),useEffect(()=>{const e=benchmarkRecorder.subscribe(e=>{"start"===e?n(!0):"stop"===e&&(n(!1),x(e=>e+1))});return n(benchmarkRecorder.isRecording()),e},[]);const b=useCallback(async()=>{if(r){const e=benchmarkRecorder.stopSession();e&&storage&&(await storage.saveReport(e),x(e=>e+1))}else{const e=(new Date).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});benchmarkRecorder.startSession({name:`Benchmark ${e}`,captureMemory:!0,verbose:!1})}},[r]),f=useCallback(async e=>{if(u)y(t=>{const o=new Set(t);return o.has(e.id)?o.delete(e.id):o.size<2&&o.add(e.id),o});else{if(!storage)return;const t=await storage.loadReport(e.id);t&&(m(t),p("detail"))}},[u]),k=useCallback(e=>{g(!0),y(new Set([e.id]))},[]),B=useCallback(async()=>{if(2!==h.size||!storage)return;const e=Array.from(h),[t,o]=await Promise.all([storage.loadReport(e[0]),storage.loadReport(e[1])]);if(t&&o){const[e,a]=t.createdAt<o.createdAt?[t,o]:[o,t];T({baseline:e,comparison:a}),p("compare"),g(!1),y(new Set)}},[h]),j=useCallback(async()=>{storage&&(await storage.clearAll(),x(e=>e+1))},[]),_=useCallback(async()=>{0!==h.size&&storage&&(await Promise.all(Array.from(h).map(e=>storage.deleteReport(e))),y(new Set),g(!1),x(e=>e+1))},[h]),$=useCallback(()=>{"detail"===d||"compare"===d?(p("list"),m(null),T(null)):u?(g(!1),y(new Set)):o&&o()},[d,u,o]),O=useCallback(()=>{c&&storage&&Alert.prompt("Rename Benchmark","Enter a new name for this benchmark:",[{text:"Cancel",style:"cancel"},{text:"Save",onPress:async e=>{e&&e.trim()&&(await storage.updateReport(c.id,{name:e.trim()}),m({...c,name:e.trim()}),x(e=>e+1))}}],"plain-text","")},[c]),w=useCallback(async()=>{c&&storage&&(await storage.deleteReport(c.id),p("list"),m(null),x(e=>e+1))},[c]),R=useCallback(async()=>{if(!S)return;const{baseline:e,comparison:t}=S,o=BenchmarkComparator.compare(e,t),a=e=>e<1?`${(1e3*e).toFixed(0)}μs`:e<10?`${e.toFixed(2)}ms`:e<100?`${e.toFixed(1)}ms`:`${e.toFixed(0)}ms`,s=e=>`${e>=0?"+":""}${e.toFixed(1)}%`,r=["BENCHMARK COMPARISON RESULTS","============================","",`Overall: ${o.isImproved?"IMPROVED":"REGRESSED"} ${s(o.overallImprovement)}`,"","COMPARING","---------",`Baseline: ${e.name}`,` Date: ${new Date(e.createdAt).toLocaleString()}`,`Comparison: ${t.name}`,` Date: ${new Date(t.createdAt).toLocaleString()}`,"","TIMING COMPARISON","-----------------",`Measure Time: ${a(e.stats.avgMeasureTime)} → ${a(t.stats.avgMeasureTime)} (${s(o.measureTimeImprovement)})`,`Pipeline Time: ${a(e.stats.avgTotalTime)} → ${a(t.stats.avgTotalTime)} (${s(o.pipelineTimeImprovement)})`,`Filter Time: ${a(e.stats.avgFilterTime)} → ${a(t.stats.avgFilterTime)} (${s(o.filterTimeImprovement)})`,`Track Time: ${a(e.stats.avgTrackTime)} → ${a(t.stats.avgTrackTime)} (${s(o.trackTimeImprovement)})`,`Overlay Render: ${a(e.stats.avgOverlayRenderTime)} → ${a(t.stats.avgOverlayRenderTime)} (${s(o.overlayRenderImprovement)})`,"","PERCENTILES","-----------",`P50 (Median): ${a(e.stats.p50TotalTime)} → ${a(t.stats.p50TotalTime)}`,`P95: ${a(e.stats.p95TotalTime)} → ${a(t.stats.p95TotalTime)}`,`P99: ${a(e.stats.p99TotalTime)} → ${a(t.stats.p99TotalTime)}`,"","BATCH STATISTICS","----------------",`Total Batches: ${e.stats.batchCount} → ${t.stats.batchCount}`,`Nodes Processed: ${e.stats.totalNodesProcessed.toLocaleString()} → ${t.stats.totalNodesProcessed.toLocaleString()}`];null!=e.memoryDelta&&null!=t.memoryDelta&&r.push("","MEMORY","------",`Memory Delta: ${(e.memoryDelta/1024/1024).toFixed(2)}MB → ${(t.memoryDelta/1024/1024).toFixed(2)}MB`),r.push("",`Compared at: ${new Date(o.comparedAt).toLocaleString()}`),await copyToClipboard(r.join("\n"))},[S]),v=useCallback(({item:e})=>_jsx(BenchmarkSessionCard,{metadata:e,isSelected:h.has(e.id),onPress:()=>f(e),onLongPress:()=>k(e),selectionMode:u}),[u,h,f,k]),M=useCallback(e=>e.id,[]),P=s?devToolsStorageKeys.modal.root():devToolsStorageKeys.benchmark.modal(),z=useMemo(()=>"detail"===d&&c?c.name:"compare"===d?"Compare":u?0===h.size?"Select 2 to compare":1===h.size?"Select 1 more":`${h.size} Selected`:"Benchmarks",[d,c,u,h]),A="list"!==d||u||o;return e?_jsxs(JsModal,{visible:e,onClose:t,onBack:A?$:void 0,onMinimize:a,persistenceKey:P,header:{showToggleButton:!0,customContent:_jsxs(ModalHeader,{children:[A&&_jsx(ModalHeader.Navigation,{onBack:$}),_jsx(ModalHeader.Content,{title:z}),_jsxs(ModalHeader.Actions,{children:["list"===d&&!u&&_jsxs(_Fragment,{children:[_jsx(PowerToggleButton,{isEnabled:r,onToggle:b,accessibilityLabel:"Toggle benchmark recording"}),i.length>=2&&_jsx(TouchableOpacity,{onPress:()=>g(!0),style:styles.iconButton,children:_jsx(GitBranch,{size:14,color:macOSColors.semantic.info})}),_jsx(TouchableOpacity,{onPress:()=>x(e=>e+1),style:styles.iconButton,children:_jsx(RefreshCw,{size:14,color:macOSColors.text.secondary})}),i.length>0&&_jsx(TouchableOpacity,{onPress:j,style:styles.iconButton,children:_jsx(Trash2,{size:14,color:macOSColors.semantic.error})})]}),"detail"===d&&c&&_jsxs(_Fragment,{children:[_jsx(TouchableOpacity,{onPress:O,style:styles.iconButton,children:_jsx(Edit3,{size:14,color:macOSColors.text.secondary})}),_jsx(TouchableOpacity,{onPress:w,style:styles.iconButton,children:_jsx(Trash2,{size:14,color:macOSColors.semantic.error})})]}),"compare"===d&&S&&_jsx(_Fragment,{children:_jsx(TouchableOpacity,{onPress:R,style:styles.iconButton,children:_jsx(Copy,{size:14,color:macOSColors.text.secondary})})}),u&&_jsxs(_Fragment,{children:[2===h.size&&_jsx(TouchableOpacity,{onPress:B,style:[styles.iconButton,styles.compareButton],children:_jsx(GitBranch,{size:14,color:macOSColors.semantic.info})}),h.size>0&&_jsx(TouchableOpacity,{onPress:_,style:styles.iconButton,children:_jsx(Trash2,{size:14,color:macOSColors.semantic.error})})]})]})]})},enablePersistence:!0,initialMode:"bottomSheet",enableGlitchEffects:!0,children:[r&&"list"===d&&_jsxs(View,{style:styles.recordingBanner,children:[_jsx(View,{style:styles.recordingDot}),_jsx(Text,{style:styles.recordingText,children:"Recording..."}),_jsx(TouchableOpacity,{onPress:b,style:styles.stopButton,children:_jsx(Text,{style:styles.stopButtonText,children:"Stop"})})]}),"detail"===d&&c?_jsx(BenchmarkDetailView,{report:c}):"compare"===d&&S?_jsx(BenchmarkCompareView,{baseline:S.baseline,comparison:S.comparison}):0===i.length?_jsxs(View,{style:styles.emptyState,children:[_jsx(Text,{style:styles.emptyIcon,children:"📊"}),_jsx(Text,{style:styles.emptyTitle,children:"No Benchmarks Yet"}),_jsx(Text,{style:styles.emptySubtitle,children:"Press the record button to start capturing performance metrics"})]}):_jsx(FlatList,{data:i,renderItem:v,keyExtractor:M,contentContainerStyle:styles.listContent,ItemSeparatorComponent:()=>_jsx(View,{style:styles.separator}),scrollEnabled:!1})]}):null}const styles=StyleSheet.create({iconButton:{width:32,height:32,borderRadius:8,backgroundColor:macOSColors.background.hover,borderWidth:1,borderColor:macOSColors.border.default,alignItems:"center",justifyContent:"center"},compareButton:{backgroundColor:macOSColors.semantic.infoBackground,borderColor:macOSColors.semantic.info},listContent:{paddingVertical:16},separator:{height:8},emptyState:{flex:1,justifyContent:"center",alignItems:"center",padding:32},emptyIcon:{fontSize:48,marginBottom:16},emptyTitle:{fontSize:16,fontWeight:"600",color:macOSColors.text.primary,fontFamily:"monospace",marginBottom:8},emptySubtitle:{fontSize:13,color:macOSColors.text.secondary,fontFamily:"monospace",textAlign:"center"},recordingBanner:{flexDirection:"row",alignItems:"center",backgroundColor:macOSColors.semantic.errorBackground,paddingHorizontal:16,paddingVertical:12,borderBottomWidth:1,borderColor:macOSColors.semantic.error},recordingDot:{width:8,height:8,borderRadius:4,backgroundColor:macOSColors.semantic.error,marginRight:8},recordingText:{flex:1,fontSize:13,fontWeight:"600",color:macOSColors.semantic.error,fontFamily:"monospace"},stopButton:{paddingHorizontal:12,paddingVertical:6,backgroundColor:macOSColors.semantic.error,borderRadius:4},stopButtonText:{fontSize:12,fontWeight:"600",color:"#fff",fontFamily:"monospace"}});export default BenchmarkModal;
1
+ "use strict";
2
+
3
+ /**
4
+ * BenchmarkModal
5
+ *
6
+ * Main modal component for the Benchmark dev tool.
7
+ * Provides recording controls, session list, detail views, and comparison.
8
+ */
9
+
10
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
11
+ import { View, Text, TouchableOpacity, StyleSheet, FlatList, Alert } from "react-native";
12
+ import { JsModal, ModalHeader, macOSColors, devToolsStorageKeys, Trash2, RefreshCw, GitBranch, Edit3, Copy, copyToClipboard, PowerToggleButton } from "@buoy-gg/shared-ui";
13
+ import { benchmarkRecorder, BenchmarkStorage, BenchmarkComparator, createAsyncStorageAdapter } from "../benchmarking";
14
+ import { BenchmarkSessionCard } from "./BenchmarkSessionCard";
15
+ import { BenchmarkDetailView } from "./BenchmarkDetailView";
16
+ import { BenchmarkCompareView } from "./BenchmarkCompareView";
17
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ // Initialize storage
19
+ const storageAdapter = createAsyncStorageAdapter();
20
+ const storage = storageAdapter ? new BenchmarkStorage(storageAdapter) : null;
21
+ export function BenchmarkModal({
22
+ visible,
23
+ onClose,
24
+ onBack,
25
+ onMinimize,
26
+ enableSharedModalDimensions = false
27
+ }) {
28
+ // State
29
+ const [isRecording, setIsRecording] = useState(false);
30
+ const [reports, setReports] = useState([]);
31
+ const [selectedReport, setSelectedReport] = useState(null);
32
+ const [viewMode, setViewMode] = useState("list");
33
+ const [selectionMode, setSelectionMode] = useState(false);
34
+ const [selectedIds, setSelectedIds] = useState(new Set());
35
+ const [compareReports, setCompareReports] = useState(null);
36
+ const [refreshKey, setRefreshKey] = useState(0);
37
+ const hasLoadedState = useRef(false);
38
+
39
+ // Load reports on mount
40
+ useEffect(() => {
41
+ if (!visible || !storage) return;
42
+ const loadReports = async () => {
43
+ const list = await storage.listReports();
44
+ setReports(list);
45
+ };
46
+ loadReports();
47
+ }, [visible, refreshKey]);
48
+
49
+ // Subscribe to recorder state changes
50
+ useEffect(() => {
51
+ const unsubscribe = benchmarkRecorder.subscribe(event => {
52
+ if (event === "start") {
53
+ setIsRecording(true);
54
+ } else if (event === "stop") {
55
+ setIsRecording(false);
56
+ // Refresh list after stopping
57
+ setRefreshKey(k => k + 1);
58
+ }
59
+ });
60
+
61
+ // Sync initial state
62
+ setIsRecording(benchmarkRecorder.isRecording());
63
+ return unsubscribe;
64
+ }, []);
65
+
66
+ // Handle start/stop recording
67
+ const handleToggleRecording = useCallback(async () => {
68
+ if (isRecording) {
69
+ // Stop and save
70
+ const report = benchmarkRecorder.stopSession();
71
+ if (report && storage) {
72
+ await storage.saveReport(report);
73
+ setRefreshKey(k => k + 1);
74
+ }
75
+ } else {
76
+ // Start new session
77
+ const timestamp = new Date().toLocaleString(undefined, {
78
+ month: "short",
79
+ day: "numeric",
80
+ hour: "2-digit",
81
+ minute: "2-digit"
82
+ });
83
+ benchmarkRecorder.startSession({
84
+ name: `Benchmark ${timestamp}`,
85
+ captureMemory: true,
86
+ verbose: false
87
+ });
88
+ }
89
+ }, [isRecording]);
90
+
91
+ // Handle report press
92
+ const handleReportPress = useCallback(async metadata => {
93
+ if (selectionMode) {
94
+ // Toggle selection
95
+ setSelectedIds(prev => {
96
+ const next = new Set(prev);
97
+ if (next.has(metadata.id)) {
98
+ next.delete(metadata.id);
99
+ } else {
100
+ // Max 2 selections for comparison
101
+ if (next.size < 2) {
102
+ next.add(metadata.id);
103
+ }
104
+ }
105
+ return next;
106
+ });
107
+ } else {
108
+ // Load and show detail
109
+ if (!storage) return;
110
+ const report = await storage.loadReport(metadata.id);
111
+ if (report) {
112
+ setSelectedReport(report);
113
+ setViewMode("detail");
114
+ }
115
+ }
116
+ }, [selectionMode]);
117
+
118
+ // Handle long press to enter selection mode
119
+ const handleReportLongPress = useCallback(metadata => {
120
+ setSelectionMode(true);
121
+ setSelectedIds(new Set([metadata.id]));
122
+ }, []);
123
+
124
+ // Handle compare button
125
+ const handleCompare = useCallback(async () => {
126
+ if (selectedIds.size !== 2 || !storage) return;
127
+ const ids = Array.from(selectedIds);
128
+ const [report1, report2] = await Promise.all([storage.loadReport(ids[0]), storage.loadReport(ids[1])]);
129
+ if (report1 && report2) {
130
+ // Older report is baseline, newer is comparison
131
+ const [baseline, comparison] = report1.createdAt < report2.createdAt ? [report1, report2] : [report2, report1];
132
+ setCompareReports({
133
+ baseline,
134
+ comparison
135
+ });
136
+ setViewMode("compare");
137
+ setSelectionMode(false);
138
+ setSelectedIds(new Set());
139
+ }
140
+ }, [selectedIds]);
141
+
142
+ // Handle clear all
143
+ const handleClearAll = useCallback(async () => {
144
+ if (storage) {
145
+ await storage.clearAll();
146
+ setRefreshKey(k => k + 1);
147
+ }
148
+ }, []);
149
+
150
+ // Handle delete selected
151
+ const handleDeleteSelected = useCallback(async () => {
152
+ if (selectedIds.size === 0 || !storage) return;
153
+ await Promise.all(Array.from(selectedIds).map(id => storage.deleteReport(id)));
154
+ setSelectedIds(new Set());
155
+ setSelectionMode(false);
156
+ setRefreshKey(k => k + 1);
157
+ }, [selectedIds]);
158
+
159
+ // Handle back navigation
160
+ const handleBack = useCallback(() => {
161
+ if (viewMode === "detail" || viewMode === "compare") {
162
+ setViewMode("list");
163
+ setSelectedReport(null);
164
+ setCompareReports(null);
165
+ } else if (selectionMode) {
166
+ setSelectionMode(false);
167
+ setSelectedIds(new Set());
168
+ } else if (onBack) {
169
+ onBack();
170
+ }
171
+ }, [viewMode, selectionMode, onBack]);
172
+
173
+ // Handle rename current report
174
+ const handleRename = useCallback(() => {
175
+ if (!selectedReport || !storage) return;
176
+ Alert.prompt("Rename Benchmark", "Enter a new name for this benchmark:", [{
177
+ text: "Cancel",
178
+ style: "cancel"
179
+ }, {
180
+ text: "Save",
181
+ onPress: async newName => {
182
+ if (newName && newName.trim()) {
183
+ await storage.updateReport(selectedReport.id, {
184
+ name: newName.trim()
185
+ });
186
+ // Update local state
187
+ setSelectedReport({
188
+ ...selectedReport,
189
+ name: newName.trim()
190
+ });
191
+ setRefreshKey(k => k + 1);
192
+ }
193
+ }
194
+ }], "plain-text", "");
195
+ }, [selectedReport]);
196
+
197
+ // Handle delete current report
198
+ const handleDeleteCurrent = useCallback(async () => {
199
+ if (!selectedReport || !storage) return;
200
+ await storage.deleteReport(selectedReport.id);
201
+ setViewMode("list");
202
+ setSelectedReport(null);
203
+ setRefreshKey(k => k + 1);
204
+ }, [selectedReport]);
205
+
206
+ // Handle copy compare results
207
+ const handleCopyCompare = useCallback(async () => {
208
+ if (!compareReports) return;
209
+ const {
210
+ baseline,
211
+ comparison
212
+ } = compareReports;
213
+ const result = BenchmarkComparator.compare(baseline, comparison);
214
+ const formatMs = ms => {
215
+ if (ms < 1) return `${(ms * 1000).toFixed(0)}μs`;
216
+ if (ms < 10) return `${ms.toFixed(2)}ms`;
217
+ if (ms < 100) return `${ms.toFixed(1)}ms`;
218
+ return `${ms.toFixed(0)}ms`;
219
+ };
220
+ const formatPercent = value => {
221
+ const sign = value >= 0 ? "+" : "";
222
+ return `${sign}${value.toFixed(1)}%`;
223
+ };
224
+ const lines = ["BENCHMARK COMPARISON RESULTS", "============================", "", `Overall: ${result.isImproved ? "IMPROVED" : "REGRESSED"} ${formatPercent(result.overallImprovement)}`, "", "COMPARING", "---------", `Baseline: ${baseline.name}`, ` Date: ${new Date(baseline.createdAt).toLocaleString()}`, `Comparison: ${comparison.name}`, ` Date: ${new Date(comparison.createdAt).toLocaleString()}`, "", "TIMING COMPARISON", "-----------------", `Measure Time: ${formatMs(baseline.stats.avgMeasureTime)} → ${formatMs(comparison.stats.avgMeasureTime)} (${formatPercent(result.measureTimeImprovement)})`, `Pipeline Time: ${formatMs(baseline.stats.avgTotalTime)} → ${formatMs(comparison.stats.avgTotalTime)} (${formatPercent(result.pipelineTimeImprovement)})`, `Filter Time: ${formatMs(baseline.stats.avgFilterTime)} → ${formatMs(comparison.stats.avgFilterTime)} (${formatPercent(result.filterTimeImprovement)})`, `Track Time: ${formatMs(baseline.stats.avgTrackTime)} → ${formatMs(comparison.stats.avgTrackTime)} (${formatPercent(result.trackTimeImprovement)})`, `Overlay Render: ${formatMs(baseline.stats.avgOverlayRenderTime)} → ${formatMs(comparison.stats.avgOverlayRenderTime)} (${formatPercent(result.overlayRenderImprovement)})`, "", "PERCENTILES", "-----------", `P50 (Median): ${formatMs(baseline.stats.p50TotalTime)} → ${formatMs(comparison.stats.p50TotalTime)}`, `P95: ${formatMs(baseline.stats.p95TotalTime)} → ${formatMs(comparison.stats.p95TotalTime)}`, `P99: ${formatMs(baseline.stats.p99TotalTime)} → ${formatMs(comparison.stats.p99TotalTime)}`, "", "BATCH STATISTICS", "----------------", `Total Batches: ${baseline.stats.batchCount} → ${comparison.stats.batchCount}`, `Nodes Processed: ${baseline.stats.totalNodesProcessed.toLocaleString()} → ${comparison.stats.totalNodesProcessed.toLocaleString()}`];
225
+ if (baseline.memoryDelta != null && comparison.memoryDelta != null) {
226
+ lines.push("", "MEMORY", "------", `Memory Delta: ${(baseline.memoryDelta / 1024 / 1024).toFixed(2)}MB → ${(comparison.memoryDelta / 1024 / 1024).toFixed(2)}MB`);
227
+ }
228
+ lines.push("", `Compared at: ${new Date(result.comparedAt).toLocaleString()}`);
229
+ await copyToClipboard(lines.join("\n"));
230
+ }, [compareReports]);
231
+
232
+ // Render list item
233
+ const renderItem = useCallback(({
234
+ item
235
+ }) => /*#__PURE__*/_jsx(BenchmarkSessionCard, {
236
+ metadata: item,
237
+ isSelected: selectedIds.has(item.id),
238
+ onPress: () => handleReportPress(item),
239
+ onLongPress: () => handleReportLongPress(item),
240
+ selectionMode: selectionMode
241
+ }), [selectionMode, selectedIds, handleReportPress, handleReportLongPress]);
242
+ const keyExtractor = useCallback(item => item.id, []);
243
+
244
+ // Render content
245
+ const renderContent = () => {
246
+ if (viewMode === "detail" && selectedReport) {
247
+ return /*#__PURE__*/_jsx(BenchmarkDetailView, {
248
+ report: selectedReport
249
+ });
250
+ }
251
+ if (viewMode === "compare" && compareReports) {
252
+ return /*#__PURE__*/_jsx(BenchmarkCompareView, {
253
+ baseline: compareReports.baseline,
254
+ comparison: compareReports.comparison
255
+ });
256
+ }
257
+
258
+ // List view
259
+ if (reports.length === 0) {
260
+ return /*#__PURE__*/_jsxs(View, {
261
+ style: styles.emptyState,
262
+ children: [/*#__PURE__*/_jsx(Text, {
263
+ style: styles.emptyIcon,
264
+ children: "\uD83D\uDCCA"
265
+ }), /*#__PURE__*/_jsx(Text, {
266
+ style: styles.emptyTitle,
267
+ children: "No Benchmarks Yet"
268
+ }), /*#__PURE__*/_jsx(Text, {
269
+ style: styles.emptySubtitle,
270
+ children: "Press the record button to start capturing performance metrics"
271
+ })]
272
+ });
273
+ }
274
+ return /*#__PURE__*/_jsx(FlatList, {
275
+ data: reports,
276
+ renderItem: renderItem,
277
+ keyExtractor: keyExtractor,
278
+ contentContainerStyle: styles.listContent,
279
+ ItemSeparatorComponent: () => /*#__PURE__*/_jsx(View, {
280
+ style: styles.separator
281
+ }),
282
+ scrollEnabled: false
283
+ });
284
+ };
285
+ const persistenceKey = enableSharedModalDimensions ? devToolsStorageKeys.modal.root() : devToolsStorageKeys.benchmark.modal();
286
+
287
+ // Header title based on view mode
288
+ const headerTitle = useMemo(() => {
289
+ if (viewMode === "detail" && selectedReport) {
290
+ return selectedReport.name;
291
+ }
292
+ if (viewMode === "compare") {
293
+ return "Compare";
294
+ }
295
+ if (selectionMode) {
296
+ if (selectedIds.size === 0) {
297
+ return "Select 2 to compare";
298
+ }
299
+ if (selectedIds.size === 1) {
300
+ return "Select 1 more";
301
+ }
302
+ return `${selectedIds.size} Selected`;
303
+ }
304
+ return "Benchmarks";
305
+ }, [viewMode, selectedReport, selectionMode, selectedIds]);
306
+
307
+ // Determine back handler
308
+ const showBackButton = viewMode !== "list" || selectionMode || onBack;
309
+
310
+ // Early return AFTER all hooks to avoid "Rendered fewer hooks than expected" error
311
+ if (!visible) return null;
312
+ return /*#__PURE__*/_jsxs(JsModal, {
313
+ visible: visible,
314
+ onClose: onClose,
315
+ onBack: showBackButton ? handleBack : undefined,
316
+ onMinimize: onMinimize,
317
+ persistenceKey: persistenceKey,
318
+ header: {
319
+ showToggleButton: true,
320
+ customContent: /*#__PURE__*/_jsxs(ModalHeader, {
321
+ children: [showBackButton && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
322
+ onBack: handleBack
323
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
324
+ title: headerTitle
325
+ }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
326
+ children: [viewMode === "list" && !selectionMode && /*#__PURE__*/_jsxs(_Fragment, {
327
+ children: [/*#__PURE__*/_jsx(PowerToggleButton, {
328
+ isEnabled: isRecording,
329
+ onToggle: handleToggleRecording,
330
+ accessibilityLabel: "Toggle benchmark recording"
331
+ }), reports.length >= 2 && /*#__PURE__*/_jsx(TouchableOpacity, {
332
+ onPress: () => setSelectionMode(true),
333
+ style: styles.iconButton,
334
+ children: /*#__PURE__*/_jsx(GitBranch, {
335
+ size: 14,
336
+ color: macOSColors.semantic.info
337
+ })
338
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
339
+ onPress: () => setRefreshKey(k => k + 1),
340
+ style: styles.iconButton,
341
+ children: /*#__PURE__*/_jsx(RefreshCw, {
342
+ size: 14,
343
+ color: macOSColors.text.secondary
344
+ })
345
+ }), reports.length > 0 && /*#__PURE__*/_jsx(TouchableOpacity, {
346
+ onPress: handleClearAll,
347
+ style: styles.iconButton,
348
+ children: /*#__PURE__*/_jsx(Trash2, {
349
+ size: 14,
350
+ color: macOSColors.semantic.error
351
+ })
352
+ })]
353
+ }), viewMode === "detail" && selectedReport && /*#__PURE__*/_jsxs(_Fragment, {
354
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
355
+ onPress: handleRename,
356
+ style: styles.iconButton,
357
+ children: /*#__PURE__*/_jsx(Edit3, {
358
+ size: 14,
359
+ color: macOSColors.text.secondary
360
+ })
361
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
362
+ onPress: handleDeleteCurrent,
363
+ style: styles.iconButton,
364
+ children: /*#__PURE__*/_jsx(Trash2, {
365
+ size: 14,
366
+ color: macOSColors.semantic.error
367
+ })
368
+ })]
369
+ }), viewMode === "compare" && compareReports && /*#__PURE__*/_jsx(_Fragment, {
370
+ children: /*#__PURE__*/_jsx(TouchableOpacity, {
371
+ onPress: handleCopyCompare,
372
+ style: styles.iconButton,
373
+ children: /*#__PURE__*/_jsx(Copy, {
374
+ size: 14,
375
+ color: macOSColors.text.secondary
376
+ })
377
+ })
378
+ }), selectionMode && /*#__PURE__*/_jsxs(_Fragment, {
379
+ children: [selectedIds.size === 2 && /*#__PURE__*/_jsx(TouchableOpacity, {
380
+ onPress: handleCompare,
381
+ style: [styles.iconButton, styles.compareButton],
382
+ children: /*#__PURE__*/_jsx(GitBranch, {
383
+ size: 14,
384
+ color: macOSColors.semantic.info
385
+ })
386
+ }), selectedIds.size > 0 && /*#__PURE__*/_jsx(TouchableOpacity, {
387
+ onPress: handleDeleteSelected,
388
+ style: styles.iconButton,
389
+ children: /*#__PURE__*/_jsx(Trash2, {
390
+ size: 14,
391
+ color: macOSColors.semantic.error
392
+ })
393
+ })]
394
+ })]
395
+ })]
396
+ })
397
+ },
398
+ enablePersistence: true,
399
+ initialMode: "bottomSheet",
400
+ enableGlitchEffects: true,
401
+ children: [isRecording && viewMode === "list" && /*#__PURE__*/_jsxs(View, {
402
+ style: styles.recordingBanner,
403
+ children: [/*#__PURE__*/_jsx(View, {
404
+ style: styles.recordingDot
405
+ }), /*#__PURE__*/_jsx(Text, {
406
+ style: styles.recordingText,
407
+ children: "Recording..."
408
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
409
+ onPress: handleToggleRecording,
410
+ style: styles.stopButton,
411
+ children: /*#__PURE__*/_jsx(Text, {
412
+ style: styles.stopButtonText,
413
+ children: "Stop"
414
+ })
415
+ })]
416
+ }), renderContent()]
417
+ });
418
+ }
419
+ const styles = StyleSheet.create({
420
+ iconButton: {
421
+ width: 32,
422
+ height: 32,
423
+ borderRadius: 8,
424
+ backgroundColor: macOSColors.background.hover,
425
+ borderWidth: 1,
426
+ borderColor: macOSColors.border.default,
427
+ alignItems: "center",
428
+ justifyContent: "center"
429
+ },
430
+ compareButton: {
431
+ backgroundColor: macOSColors.semantic.infoBackground,
432
+ borderColor: macOSColors.semantic.info
433
+ },
434
+ listContent: {
435
+ paddingVertical: 16
436
+ },
437
+ separator: {
438
+ height: 8
439
+ },
440
+ emptyState: {
441
+ flex: 1,
442
+ justifyContent: "center",
443
+ alignItems: "center",
444
+ padding: 32
445
+ },
446
+ emptyIcon: {
447
+ fontSize: 48,
448
+ marginBottom: 16
449
+ },
450
+ emptyTitle: {
451
+ fontSize: 16,
452
+ fontWeight: "600",
453
+ color: macOSColors.text.primary,
454
+ fontFamily: "monospace",
455
+ marginBottom: 8
456
+ },
457
+ emptySubtitle: {
458
+ fontSize: 13,
459
+ color: macOSColors.text.secondary,
460
+ fontFamily: "monospace",
461
+ textAlign: "center"
462
+ },
463
+ recordingBanner: {
464
+ flexDirection: "row",
465
+ alignItems: "center",
466
+ backgroundColor: macOSColors.semantic.errorBackground,
467
+ paddingHorizontal: 16,
468
+ paddingVertical: 12,
469
+ borderBottomWidth: 1,
470
+ borderColor: macOSColors.semantic.error
471
+ },
472
+ recordingDot: {
473
+ width: 8,
474
+ height: 8,
475
+ borderRadius: 4,
476
+ backgroundColor: macOSColors.semantic.error,
477
+ marginRight: 8
478
+ },
479
+ recordingText: {
480
+ flex: 1,
481
+ fontSize: 13,
482
+ fontWeight: "600",
483
+ color: macOSColors.semantic.error,
484
+ fontFamily: "monospace"
485
+ },
486
+ stopButton: {
487
+ paddingHorizontal: 12,
488
+ paddingVertical: 6,
489
+ backgroundColor: macOSColors.semantic.error,
490
+ borderRadius: 4
491
+ },
492
+ stopButtonText: {
493
+ fontSize: 12,
494
+ fontWeight: "600",
495
+ color: "#fff",
496
+ fontFamily: "monospace"
497
+ }
498
+ });
499
+ export default BenchmarkModal;