Michael Rabinovich commited on
Commit
f2f35be
·
1 Parent(s): 4ee70ef

app: fix leaderboard auto-refresh via @gr .render(key=...)

Browse files

In Gradio 6.14 a `gr.Dataframe` identity-checks its component id and
short-circuits the diff when the new value's shape + column structure
look unchanged, even when the row data differs. That swallowed
updates from `every=` on the Dataframe AND from
`gr.Timer().tick(outputs=df_view)` AND from a manual Refresh button
click: the server-side fetch ran every tick (`Loaded 10 rows from
Hub` in logs) but the browser never saw the new rows. Only a full
page reload picked them up.

Documented fix (post Gradio's March 2026 Dataframe overhaul): wrap
the component in `@gr.render` and give the inner Dataframe a `key=`
that changes on every tick. Gradio then tears down and rebuilds the
component subtree in place, picking up the fresh value instead of
trying to diff in place.

Shape:
- tick_state: gr.State holding a ms-timestamp that bumps on every
refresh; included in the Dataframe `key=`.
- table_state: gr.State holding the pandas DataFrame.
- `_refresh_table()` runs once per tick, returns
(new_df, new_tick); writes both via outputs=[table_state,
tick_state]. One server-side fetch shared across all subscribers
per tick instead of N viewers x N fetches.
- `@gr.render(inputs=[table_state, tick_state])` rebuilds the
Dataframe with `key=f"leaderboard-df-{t}"` each tick.
- gr.Timer(10).tick wires _refresh_table; Refresh button click
wires the same function.

Verified locally against the live submissions dataset: 10 rows render
correctly (including the latest `Auto-refresh test BETA` row);
server-side Timer ticks every 10s (`Loaded 10 rows from Hub` lines
in the local log).

Diagnosis credit: Leandro reviewed the symptoms and pointed at the
Gradio 6 identity-diff failure mode + the @gr .render(key=) workaround;
this is his pattern.

Files changed (1) hide show
  1. app.py +36 -12
app.py CHANGED
@@ -6,6 +6,7 @@ Read path lives in :mod:`leaderboard`. Submit-tab validation lives in
6
  from __future__ import annotations
7
 
8
  import logging
 
9
 
10
  import gradio as gr
11
 
@@ -49,20 +50,43 @@ with gr.Blocks(title="CADGenBench Leaderboard") as app:
49
  )
50
 
51
  with gr.Tab("Leaderboard"):
52
- df_view = gr.Dataframe(
53
- value=load_leaderboard,
54
- interactive=False,
55
- wrap=True,
56
- label="Results (sorted by aggregate CAD score)",
57
- )
58
- # gr.Dataframe's built-in `every=` doesn't reliably re-render
59
- # when rows are added between ticks (gradio-app/gradio#8160).
60
- # Drive periodic updates through an explicit gr.Timer event,
61
- # same code path as the manual Refresh button.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  auto_refresh_timer = gr.Timer(10)
63
- auto_refresh_timer.tick(fn=load_leaderboard, outputs=df_view)
 
 
64
  refresh_btn = gr.Button("Refresh", size="sm")
65
- refresh_btn.click(fn=load_leaderboard, outputs=df_view)
 
 
66
 
67
  with gr.Tab("Submit"):
68
  gr.Markdown(
 
6
  from __future__ import annotations
7
 
8
  import logging
9
+ import time
10
 
11
  import gradio as gr
12
 
 
50
  )
51
 
52
  with gr.Tab("Leaderboard"):
53
+ # Gradio 6's gr.Dataframe identity-checks its component id and
54
+ # short-circuits the diff when the new value's shape + column
55
+ # structure look unchanged, even though the row data differs.
56
+ # That swallows updates from every= on the Dataframe AND from
57
+ # gr.Timer().tick(outputs=df_view) AND from a manual refresh
58
+ # button click. The fix is @gr.render with a `key=` that
59
+ # changes on every tick: Gradio tears down and rebuilds the
60
+ # component subtree in place, picking up the fresh value.
61
+ #
62
+ # The fetch happens once per tick in `_refresh_table` (server
63
+ # side) and the result rides on gr.State to all subscribers,
64
+ # so N concurrent viewers don't cause N HTTPS GETs per tick.
65
+ table_state = gr.State(value=load_leaderboard())
66
+ tick_state = gr.State(value=0)
67
+
68
+ @gr.render(inputs=[table_state, tick_state])
69
+ def render_leaderboard(df, t: int) -> None:
70
+ gr.Dataframe(
71
+ value=df,
72
+ interactive=False,
73
+ wrap=True,
74
+ label="Results (sorted by aggregate CAD score)",
75
+ key=f"leaderboard-df-{t}",
76
+ )
77
+
78
+ def _refresh_table():
79
+ # ms-resolution so rapid clicks always increment the key.
80
+ return load_leaderboard(), int(time.time() * 1000)
81
+
82
  auto_refresh_timer = gr.Timer(10)
83
+ auto_refresh_timer.tick(
84
+ fn=_refresh_table, outputs=[table_state, tick_state],
85
+ )
86
  refresh_btn = gr.Button("Refresh", size="sm")
87
+ refresh_btn.click(
88
+ fn=_refresh_table, outputs=[table_state, tick_state],
89
+ )
90
 
91
  with gr.Tab("Submit"):
92
  gr.Markdown(