Skip to content

Commit 1376ba3

Browse files
authored
chore: use a cache to select best latency (#7879)
* chore: Testing using a cache to choose the best latency * Allow storing more latencies if needed This PR enables a feature to record and save latencies to local storage for later analysis
1 parent 74ffd27 commit 1376ba3

File tree

1 file changed

+111
-3
lines changed

1 file changed

+111
-3
lines changed

site/src/contexts/useProxyLatency.ts

+111-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,32 @@ const proxyLatenciesReducer = (
2424
state: Record<string, ProxyLatencyReport>,
2525
action: ProxyLatencyAction,
2626
): Record<string, ProxyLatencyReport> => {
27+
// TODO: We should probably not read from local storage on every action.
28+
const history = loadStoredLatencies()
29+
const proxyHistory = history[action.proxyID] || []
30+
const minReport = proxyHistory.reduce((min, report) => {
31+
if (min.latencyMS === 0) {
32+
// Not yet set, so use the new report.
33+
return report
34+
}
35+
if (min.latencyMS < report.latencyMS) {
36+
return min
37+
}
38+
return report
39+
}, {} as ProxyLatencyReport)
40+
41+
if (
42+
minReport.latencyMS > 0 &&
43+
minReport.latencyMS < action.report.latencyMS
44+
) {
45+
// The new report is slower then the min report, so use the min report.
46+
return {
47+
...state,
48+
[action.proxyID]: minReport,
49+
}
50+
}
51+
52+
// Use the new report
2753
return {
2854
...state,
2955
[action.proxyID]: action.report,
@@ -38,6 +64,18 @@ export const useProxyLatency = (
3864
refetch: () => void
3965
proxyLatencies: Record<string, ProxyLatencyReport>
4066
} => {
67+
// maxStoredLatencies is the maximum number of latencies to store per proxy in local storage.
68+
let maxStoredLatencies = 8
69+
// The reason we pull this from local storage is so for development purposes, a user can manually
70+
// set a larger number to collect data in their normal usage. This data can later be analyzed to come up
71+
// with some better magic numbers.
72+
const maxStoredLatenciesVar = localStorage.getItem(
73+
"workspace-proxy-latencies-max",
74+
)
75+
if (maxStoredLatenciesVar) {
76+
maxStoredLatencies = Number(maxStoredLatenciesVar)
77+
}
78+
4179
const [proxyLatencies, dispatchProxyLatencies] = useReducer(
4280
proxyLatenciesReducer,
4381
{},
@@ -113,14 +151,17 @@ export const useProxyLatency = (
113151
)
114152
latencyMS = entry.duration
115153
}
116-
dispatchProxyLatencies({
154+
const update = {
117155
proxyID: check.id,
118156
report: {
119157
latencyMS,
120158
accurate,
121159
at: new Date(),
122160
},
123-
})
161+
}
162+
dispatchProxyLatencies(update)
163+
// Also save to local storage to persist the latency across page refreshes.
164+
updateStoredLatencies(update)
124165

125166
return
126167
}
@@ -140,6 +181,10 @@ export const useProxyLatency = (
140181
const proxyRequests = Object.keys(proxyChecks).map((latencyURL) => {
141182
return axios.get(latencyURL, {
142183
withCredentials: false,
184+
// Must add a custom header to make the request not a "simple request".
185+
// We want to force a preflight request.
186+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
187+
headers: { "X-LATENCY-CHECK": "true" },
143188
})
144189
})
145190

@@ -156,11 +201,74 @@ export const useProxyLatency = (
156201
// At this point, we can be confident that all the proxy requests have been recorded
157202
// via the performance observer. So we can disconnect the observer.
158203
observer.disconnect()
204+
205+
// Local storage cleanup
206+
garbageCollectStoredLatencies(proxies, maxStoredLatencies)
159207
})
160-
}, [proxies, latestFetchRequest])
208+
}, [proxies, latestFetchRequest, maxStoredLatencies])
161209

162210
return {
163211
proxyLatencies,
164212
refetch,
165213
}
166214
}
215+
216+
// Local storage functions
217+
218+
// loadStoredLatencies will load the stored latencies from local storage.
219+
// Latencies are stored in local storage to minimize the impact of outliers.
220+
// If a single request is slow, we want to omit that latency check, and go with
221+
// a more accurate latency check.
222+
const loadStoredLatencies = (): Record<string, ProxyLatencyReport[]> => {
223+
const str = localStorage.getItem("workspace-proxy-latencies")
224+
if (!str) {
225+
return {}
226+
}
227+
228+
return JSON.parse(str)
229+
}
230+
231+
const updateStoredLatencies = (action: ProxyLatencyAction): void => {
232+
const latencies = loadStoredLatencies()
233+
const reports = latencies[action.proxyID] || []
234+
235+
reports.push(action.report)
236+
latencies[action.proxyID] = reports
237+
localStorage.setItem("workspace-proxy-latencies", JSON.stringify(latencies))
238+
}
239+
240+
// garbageCollectStoredLatencies will remove any latencies that are older then 1 week or latencies of proxies
241+
// that no longer exist. This is intended to keep the size of local storage down.
242+
const garbageCollectStoredLatencies = (
243+
regions: RegionsResponse,
244+
maxStored: number,
245+
): void => {
246+
const latencies = loadStoredLatencies()
247+
const now = Date.now()
248+
const cleaned = cleanupLatencies(latencies, regions, new Date(now), maxStored)
249+
250+
localStorage.setItem("workspace-proxy-latencies", JSON.stringify(cleaned))
251+
}
252+
253+
const cleanupLatencies = (
254+
stored: Record<string, ProxyLatencyReport[]>,
255+
regions: RegionsResponse,
256+
now: Date,
257+
maxStored: number,
258+
): Record<string, ProxyLatencyReport[]> => {
259+
Object.keys(stored).forEach((proxyID) => {
260+
if (!regions.regions.find((region) => region.id === proxyID)) {
261+
delete stored[proxyID]
262+
return
263+
}
264+
const reports = stored[proxyID]
265+
const nowMS = now.getTime()
266+
stored[proxyID] = reports.filter((report) => {
267+
// Only keep the reports that are less then 1 week old.
268+
return new Date(report.at).getTime() > nowMS - 1000 * 60 * 60 * 24 * 7
269+
})
270+
// Only keep the 5 latest
271+
stored[proxyID] = stored[proxyID].slice(-1 * maxStored)
272+
})
273+
return stored
274+
}

0 commit comments

Comments
 (0)