Repository navigation
Expand file tree
/
Copy pathdiagnostic-log.ts
More file actions
144 lines (131 loc) · 3.92 KB
/
Copy pathdiagnostic-log.ts
File metadata and controls
144 lines (131 loc) · 3.92 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
export type DiagnosticCategory =
| "APP"
| "INPUT"
| "LOCATION"
| "STORAGE"
| "LIVE"
| "REFRESH"
| "ENCODE"
| "TILE"
| "TIMER"
| "ERROR";
export type DiagnosticEntry = {
readonly sequence: number;
readonly timestamp: string;
readonly category: DiagnosticCategory;
readonly message: string;
readonly durationMs?: number;
};
type LoggerOptions = {
readonly capacity?: number;
readonly now?: () => Date;
};
function formatTimestamp(date: Date): string {
const part = (value: number, width = 2) => (
String(value).padStart(width, "0")
);
return `${part(date.getHours())}:${part(date.getMinutes())}:`
+ `${part(date.getSeconds())}.${part(date.getMilliseconds(), 3)}`;
}
function formatEntry(entry: DiagnosticEntry): string {
return `[${entry.timestamp}] #${String(entry.sequence).padStart(4, "0")}`
+ ` [${entry.category}] ${entry.message}`
+ (entry.durationMs === undefined ? "" : ` · ${entry.durationMs}ms`);
}
export function createDiagnosticLogger(options: LoggerOptions = {}) {
const capacity = Math.max(1, Math.floor(options.capacity ?? 300));
const now = options.now ?? (() => new Date());
let entries: DiagnosticEntry[] = [];
let sequence = 0;
let dropped = 0;
let refreshDropped = 0;
let version = 0;
const snapshot = () => ({
version,
dropped,
refreshDropped,
capacity,
entries: entries.map((entry) => ({ ...entry })),
});
return {
append(
category: DiagnosticCategory,
message: string,
durationMs?: number,
) {
sequence += 1;
entries.push({
sequence,
timestamp: formatTimestamp(now()),
category,
message,
...(durationMs === undefined
? {}
: { durationMs: Math.max(0, Math.round(durationMs)) }),
});
if (category === "REFRESH" && /\bdropped\b/.test(message)) {
refreshDropped += 1;
}
if (entries.length > capacity) {
entries = entries.slice(entries.length - capacity);
dropped += 1;
}
version += 1;
},
clear() {
entries = [];
dropped = 0;
refreshDropped = 0;
version += 1;
},
snapshot,
text: () => entries.map(formatEntry).join("\n"),
version: () => version,
};
}
export type DiagnosticLogger = ReturnType<typeof createDiagnosticLogger>;
export const diagnosticLogger = createDiagnosticLogger();
export const logDiagnostic = diagnosticLogger.append;
type HeartbeatOptions = {
readonly intervalMs?: number;
readonly now?: () => number;
};
export function startDiagnosticHeartbeat(
logger: DiagnosticLogger = diagnosticLogger,
options: HeartbeatOptions = {},
): () => void {
const intervalMs = options.intervalMs ?? 5_000;
const now = options.now ?? Date.now;
let expected = now() + intervalMs;
let stopped = false;
logger.append("TIMER", `heartbeat started · interval ${intervalMs}ms`);
const timer = globalThis.setInterval(() => {
const current = now();
const drift = Math.max(0, Math.round(current - expected));
logger.append("TIMER", `heartbeat · drift ${drift}ms`);
expected = current + intervalMs;
}, intervalMs);
return () => {
if (stopped) return;
stopped = true;
globalThis.clearInterval(timer);
logger.append("TIMER", "heartbeat stopped");
};
}
function errorKind(error: unknown): string {
return error instanceof Error ? error.name : typeof error;
}
export function startWindowErrorDiagnostics(): () => void {
const ErrorEvent) => {
logDiagnostic("ERROR", `window error · ${errorKind(event.error)}`);
};
const PromiseRejectionEvent) => {
logDiagnostic("ERROR", `unhandled rejection · ${errorKind(event.reason)}`);
};
window.addEventListener("error", onError);
window.addEventListener("unhandledrejection", onRejection);
return () => {
window.removeEventListener("error", onError);
window.removeEventListener("unhandledrejection", onRejection);
};
}