Skip to content

Commit d54d290

Browse files
abidlabsgradio-pr-botclaude
authored
Reduce HF Spaces 429s: polling tuning and batched metric logs API (#513)
Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 60bbc86 commit d54d290

9 files changed

Lines changed: 652 additions & 93 deletions

File tree

.changeset/polite-tigers-drop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trackio": minor
3+
---
4+
5+
feat:Reduce HF Spaces 429s: polling tuning and batched metric logs API

docs/source/track.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ If both a Space and a self-hosted URL are configured (`space_id` / `TRACKIO_SPAC
5555

5656
For setup steps (running `trackio show`, binding to `0.0.0.0`, write tokens), see [Self-host the Server](self_hosted_server.md).
5757

58+
The built-in dashboard polls for new runs and metrics every **1 second** on localhost and every **2 seconds** when opened on a Hugging Face Space (`*.hf.space`), to ease rate limits on the Space URL.
59+
5860
## Logging Data
5961

6062
Once your run is initialized, you can start logging data using the [`log`] function:

trackio/frontend/src/App.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
isStaticMode,
2121
setMediaDir,
2222
} from "./lib/api.js";
23+
import {
24+
getAppPollIntervalMs,
25+
isRateLimitCooldownActive,
26+
isTabHidden,
27+
} from "./lib/hostPolling.js";
2328
import { setColorPalette } from "./lib/stores.js";
2429
import { getPageFromPath, navigateTo, getQueryParam } from "./lib/router.js";
2530
import Settings from "./pages/Settings.svelte";
@@ -159,9 +164,11 @@
159164
if (pollTimer) clearInterval(pollTimer);
160165
pollTimer = setInterval(async () => {
161166
if (!realtimeEnabled) return;
167+
if (isTabHidden()) return;
168+
if (isRateLimitCooldownActive()) return;
162169
await refreshRuns();
163170
await refreshAlerts();
164-
}, 1000);
171+
}, getAppPollIntervalMs());
165172
}
166173
167174
function applyUrlTokens() {
@@ -382,6 +389,7 @@
382389
{showHeaders}
383390
{appBootstrapReady}
384391
{plotOrder}
392+
{realtimeEnabled}
385393
bind:metricColumns
386394
/>
387395
{:else if currentPage === "system"}
@@ -390,6 +398,7 @@
390398
selectedRuns={selectedRunRecords}
391399
{smoothing}
392400
{appBootstrapReady}
401+
{realtimeEnabled}
393402
bind:availableDevices={availableSystemDevices}
394403
bind:selectedDevices={selectedSystemDevices}
395404
/>

trackio/frontend/src/lib/api.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as staticApi from "./staticApi.js";
2+
import { registerRateLimitHit } from "./hostPolling.js";
23

34
const BASE = window.__trackio_base || "";
45

@@ -47,6 +48,9 @@ export async function callApi(apiName, params = {}) {
4748
headers: { "Content-Type": "application/json", ...getOauthSessionHeader() },
4849
body: JSON.stringify(params),
4950
});
51+
if (resp.status === 429) {
52+
registerRateLimitHit();
53+
}
5054
if (!resp.ok) {
5155
throw new Error(`API call ${apiName} failed: ${resp.status}`);
5256
}
@@ -85,6 +89,29 @@ export async function getLogs(project, run) {
8589
return await callApi("/get_logs", params);
8690
}
8791

92+
export async function getLogsBatch(project, runs) {
93+
if (await isStaticMode()) {
94+
const out = [];
95+
for (const run of runs) {
96+
const logs = await staticApi.getLogs(project, run);
97+
out.push({
98+
run: run?.name ?? null,
99+
run_id: run?.id ?? null,
100+
logs,
101+
});
102+
}
103+
return out;
104+
}
105+
const payload = {
106+
project,
107+
runs: runs.map((run) => ({
108+
run: run?.name ?? null,
109+
run_id: run?.id ?? null,
110+
})),
111+
};
112+
return await callApi("/get_logs_batch", payload);
113+
}
114+
88115
export async function getProjectSummary(project) {
89116
if (await isStaticMode()) return staticApi.getProjectSummary(project);
90117
return await callApi("/get_project_summary", { project });
@@ -114,6 +141,28 @@ export async function getSystemLogs(project, run) {
114141
return await callApi("/get_system_logs", params);
115142
}
116143

144+
export async function getSystemLogsBatch(project, runs) {
145+
if (await isStaticMode()) {
146+
const out = [];
147+
for (const run of runs) {
148+
const logs = await staticApi.getSystemLogs(project, run);
149+
out.push({
150+
run: run?.name ?? null,
151+
run_id: run?.id ?? null,
152+
logs,
153+
});
154+
}
155+
return out;
156+
}
157+
return await callApi("/get_system_logs_batch", {
158+
project,
159+
runs: runs.map((run) => ({
160+
run: run?.name ?? null,
161+
run_id: run?.id ?? null,
162+
})),
163+
});
164+
}
165+
117166
export async function getSnapshot(project, run, step) {
118167
const params = { project, ...normalizeRun(run) };
119168
if (await isStaticMode()) return staticApi.getSnapshot(project, run, step);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
let rateLimitCooldownUntil = 0;
2+
3+
export function isHfSpaceHost() {
4+
if (typeof window === "undefined") return false;
5+
return (window.location.hostname || "")
6+
.toLowerCase()
7+
.endsWith(".hf.space");
8+
}
9+
10+
export function registerRateLimitHit() {
11+
const until = Date.now() + 12000;
12+
rateLimitCooldownUntil = Math.max(rateLimitCooldownUntil, until);
13+
}
14+
15+
export function isRateLimitCooldownActive() {
16+
return Date.now() < rateLimitCooldownUntil;
17+
}
18+
19+
export function getAppPollIntervalMs() {
20+
return isHfSpaceHost() ? 2500 : 1000;
21+
}
22+
23+
export function getMetricsPollIntervalMs() {
24+
return isHfSpaceHost() ? 3500 : 1000;
25+
}
26+
27+
export function isTabHidden() {
28+
return typeof document !== "undefined" && document.hidden;
29+
}

trackio/frontend/src/pages/Metrics.svelte

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
import BarPlot from "../components/BarPlot.svelte";
66
import Accordion from "../components/Accordion.svelte";
77
import LoadingTrackio from "../components/LoadingTrackio.svelte";
8-
import { getLogs } from "../lib/api.js";
8+
import { getLogsBatch } from "../lib/api.js";
9+
import {
10+
getMetricsPollIntervalMs,
11+
isRateLimitCooldownActive,
12+
isTabHidden,
13+
} from "../lib/hostPolling.js";
914
import {
1015
processRunData,
1116
getMetricColumns,
@@ -27,6 +32,7 @@
2732
showHeaders = true,
2833
appBootstrapReady = false,
2934
plotOrder = [],
35+
realtimeEnabled = true,
3036
// eslint-disable-next-line no-useless-assignment -- bindable out-prop to parent
3137
metricColumns = $bindable([]),
3238
} = $props();
@@ -168,13 +174,21 @@
168174
return;
169175
}
170176
171-
let fetched = false;
172-
for (const run of selectedRuns) {
177+
const needFetch = selectedRuns.filter((run) => {
173178
const runKey = run.id ?? run.name;
174-
if (!rawDataCache.has(runKey)) {
175-
const logs = await getLogs(project, run);
176-
rawDataCache.set(runKey, logs);
177-
fetched = true;
179+
return !rawDataCache.has(runKey);
180+
});
181+
let fetched = false;
182+
if (needFetch.length > 0) {
183+
try {
184+
const batch = await getLogsBatch(project, needFetch);
185+
for (const entry of batch) {
186+
const runKey = entry.run_id ?? entry.run;
187+
rawDataCache.set(runKey, entry.logs);
188+
fetched = true;
189+
}
190+
} catch (e) {
191+
console.error("Failed to load metric logs:", e);
178192
}
179193
}
180194
@@ -185,20 +199,28 @@
185199
}
186200
187201
async function refreshCachedRuns() {
202+
if (!realtimeEnabled) return;
188203
if (!project || selectedRuns.length === 0) return;
204+
if (isTabHidden()) return;
205+
if (isRateLimitCooldownActive()) return;
189206
190-
let changed = false;
191-
for (const run of selectedRuns) {
192-
const logs = await getLogs(project, run);
193-
const runKey = run.id ?? run.name;
194-
const prev = rawDataCache.get(runKey);
195-
if (!prev || logs.length !== prev.length) {
196-
rawDataCache.set(runKey, logs);
197-
changed = true;
207+
try {
208+
const batch = await getLogsBatch(project, selectedRuns);
209+
let changed = false;
210+
for (const entry of batch) {
211+
const runKey = entry.run_id ?? entry.run;
212+
const logs = entry.logs;
213+
const prev = rawDataCache.get(runKey);
214+
if (!prev || logs.length !== prev.length) {
215+
rawDataCache.set(runKey, logs);
216+
changed = true;
217+
}
198218
}
199-
}
200-
if (changed) {
201-
processFromCache();
219+
if (changed) {
220+
processFromCache();
221+
}
222+
} catch (e) {
223+
console.error("Failed to refresh metric logs:", e);
202224
}
203225
}
204226
@@ -230,7 +252,10 @@
230252
xLim = [lo, hi];
231253
}
232254
}
233-
refreshTimer = setInterval(refreshCachedRuns, 1000);
255+
refreshTimer = setInterval(
256+
refreshCachedRuns,
257+
getMetricsPollIntervalMs(),
258+
);
234259
return () => {
235260
if (refreshTimer) clearInterval(refreshTimer);
236261
};

0 commit comments

Comments
 (0)