-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathperformance.go
More file actions
236 lines (203 loc) · 6.71 KB
/
performance.go
File metadata and controls
236 lines (203 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
package w3pilot
import (
"context"
"encoding/json"
"fmt"
)
// PerformanceMetrics contains Core Web Vitals and navigation timing.
type PerformanceMetrics struct {
// Core Web Vitals
LCP float64 `json:"lcp,omitempty"` // Largest Contentful Paint (ms)
CLS float64 `json:"cls,omitempty"` // Cumulative Layout Shift
INP float64 `json:"inp,omitempty"` // Interaction to Next Paint (ms)
FID float64 `json:"fid,omitempty"` // First Input Delay (ms)
FCP float64 `json:"fcp,omitempty"` // First Contentful Paint (ms)
TTFB float64 `json:"ttfb,omitempty"` // Time to First Byte (ms)
// Navigation Timing
DOMContentLoaded float64 `json:"domContentLoaded,omitempty"` // DOMContentLoaded event (ms)
Load float64 `json:"load,omitempty"` // Load event (ms)
DOMInteractive float64 `json:"domInteractive,omitempty"` // DOM interactive (ms)
// Resource Timing
ResourceCount int `json:"resourceCount,omitempty"` // Number of resources loaded
}
// MemoryStats contains JavaScript heap memory information.
type MemoryStats struct {
UsedJSHeapSize int64 `json:"usedJSHeapSize"` // Used JS heap size in bytes
TotalJSHeapSize int64 `json:"totalJSHeapSize"` // Total JS heap size in bytes
JSHeapSizeLimit int64 `json:"jsHeapSizeLimit"` // JS heap size limit in bytes
}
// GetPerformanceMetrics retrieves Core Web Vitals and navigation timing.
// Note: Some metrics (LCP, CLS, INP) require user interaction or time to measure.
func (p *Pilot) GetPerformanceMetrics(ctx context.Context) (*PerformanceMetrics, error) {
script := `
(function() {
const metrics = {};
// Navigation Timing
const nav = performance.getEntriesByType('navigation')[0];
if (nav) {
metrics.ttfb = nav.responseStart - nav.requestStart;
metrics.domContentLoaded = nav.domContentLoadedEventEnd - nav.startTime;
metrics.load = nav.loadEventEnd - nav.startTime;
metrics.domInteractive = nav.domInteractive - nav.startTime;
}
// First Contentful Paint
const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0];
if (fcpEntry) {
metrics.fcp = fcpEntry.startTime;
}
// Largest Contentful Paint (if available)
const lcpEntries = performance.getEntriesByType('largest-contentful-paint');
if (lcpEntries.length > 0) {
metrics.lcp = lcpEntries[lcpEntries.length - 1].startTime;
}
// Layout Shift (CLS)
let cls = 0;
const layoutShiftEntries = performance.getEntriesByType('layout-shift');
for (const entry of layoutShiftEntries) {
if (!entry.hadRecentInput) {
cls += entry.value;
}
}
if (layoutShiftEntries.length > 0) {
metrics.cls = cls;
}
// Resource count
metrics.resourceCount = performance.getEntriesByType('resource').length;
return metrics;
})()
`
result, err := p.Evaluate(ctx, script)
if err != nil {
return nil, fmt.Errorf("w3pilot: failed to get performance metrics: %w", err)
}
// Convert result to JSON and unmarshal
jsonBytes, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("w3pilot: failed to marshal performance metrics: %w", err)
}
var metrics PerformanceMetrics
if err := json.Unmarshal(jsonBytes, &metrics); err != nil {
return nil, fmt.Errorf("w3pilot: failed to parse performance metrics: %w", err)
}
return &metrics, nil
}
// GetMemoryStats retrieves JavaScript heap memory information.
// Note: This requires Chrome with --enable-precise-memory-info flag.
func (p *Pilot) GetMemoryStats(ctx context.Context) (*MemoryStats, error) {
script := `
(function() {
if (!performance.memory) {
return null;
}
return {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
};
})()
`
result, err := p.Evaluate(ctx, script)
if err != nil {
return nil, fmt.Errorf("w3pilot: failed to get memory stats: %w", err)
}
if result == nil {
return nil, fmt.Errorf("w3pilot: performance.memory not available (requires --enable-precise-memory-info)")
}
// Convert result to JSON and unmarshal
jsonBytes, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("w3pilot: failed to marshal memory stats: %w", err)
}
var stats MemoryStats
if err := json.Unmarshal(jsonBytes, &stats); err != nil {
return nil, fmt.Errorf("w3pilot: failed to parse memory stats: %w", err)
}
return &stats, nil
}
// ObserveWebVitals starts observing Core Web Vitals in real-time.
// Returns a channel that receives metrics as they are measured.
// Call the returned cancel function to stop observing.
func (p *Pilot) ObserveWebVitals(ctx context.Context) (<-chan *PerformanceMetrics, func(), error) {
// Inject the observer script
observerScript := `
window.__w3pilotMetrics = window.__w3pilotMetrics || { lcp: 0, cls: 0, inp: 0, fid: 0 };
// LCP Observer
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
window.__w3pilotMetrics.lcp = lastEntry.startTime;
}).observe({ type: 'largest-contentful-paint', buffered: true });
// CLS Observer
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
window.__w3pilotMetrics.cls += entry.value;
}
}
}).observe({ type: 'layout-shift', buffered: true });
// FID Observer
new PerformanceObserver((list) => {
const firstEntry = list.getEntries()[0];
if (firstEntry) {
window.__w3pilotMetrics.fid = firstEntry.processingStart - firstEntry.startTime;
}
}).observe({ type: 'first-input', buffered: true });
// INP Observer (via event timing)
let maxINP = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.interactionId) {
const duration = entry.duration;
if (duration > maxINP) {
maxINP = duration;
window.__w3pilotMetrics.inp = duration;
}
}
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });
true
`
if _, err := p.Evaluate(ctx, observerScript); err != nil {
return nil, nil, fmt.Errorf("w3pilot: failed to start web vitals observer: %w", err)
}
// Create channel and start polling
ch := make(chan *PerformanceMetrics, 10)
done := make(chan struct{})
go func() {
defer close(ch)
for {
select {
case <-done:
return
case <-ctx.Done():
return
default:
// Poll metrics
result, err := p.Evaluate(ctx, "window.__w3pilotMetrics")
if err == nil && result != nil {
jsonBytes, err := json.Marshal(result)
if err == nil {
var metrics PerformanceMetrics
if json.Unmarshal(jsonBytes, &metrics) == nil {
select {
case ch <- &metrics:
default: // Don't block if channel is full
}
}
}
}
// Wait before next poll
select {
case <-done:
return
case <-ctx.Done():
return
}
}
}
}()
cancel := func() {
close(done)
}
return ch, cancel, nil
}