Skip to content

Commit 1d5994d

Browse files
authored
FixDataframe performance when rendering many datetime rows (#13305)
* fix perf * fix * fix changeset
1 parent 2fdc8cf commit 1d5994d

4 files changed

Lines changed: 180 additions & 24 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@gradio/dataframe": patch
3+
"gradio": patch
4+
---
5+
6+
fix:Dataframe: fix extreme rendering slowdown with `datatype="date"` (and any future dtype with asymmetric string casts) by firing `EditableCell`'s shim-blur only on edit teardown instead of every render. Also makes the hidden sizing-row computation faster by avoiding Date rendering for every entry.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import random
2+
3+
import gradio as gr
4+
5+
ROWS = 5000
6+
rng = random.Random(42)
7+
8+
headers = [
9+
"date",
10+
"str_short",
11+
"str_long",
12+
"num",
13+
"bool",
14+
"markdown",
15+
"html",
16+
]
17+
datatype = ["date", "str", "str", "number", "bool", "markdown", "html"]
18+
19+
WORDS = [
20+
"alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta",
21+
"iota", "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi",
22+
]
23+
24+
MD_WRAPPERS = [
25+
lambda s: f"**{s}**",
26+
lambda s: f"*{s}*",
27+
lambda s: f"`{s}`",
28+
lambda s: f"[{s}](https://example.com)",
29+
lambda s: f"# {s}",
30+
lambda s: s,
31+
]
32+
33+
HTML_WRAPPERS = [
34+
lambda s: f"<b>{s}</b>",
35+
lambda s: f"<i>{s}</i>",
36+
lambda s: f'<span style="color: tomato">{s}</span>',
37+
lambda s: f'<a href="https://example.com" target="_blank">{s}</a>',
38+
lambda s: f"<code>{s}</code>",
39+
lambda s: s,
40+
]
41+
42+
43+
def random_text(min_words: int, max_words: int) -> str:
44+
n = rng.randint(min_words, max_words)
45+
return " ".join(rng.choice(WORDS) for _ in range(n))
46+
47+
48+
def random_md() -> str:
49+
return rng.choice(MD_WRAPPERS)(random_text(1, 8))
50+
51+
52+
def random_html() -> str:
53+
return rng.choice(HTML_WRAPPERS)(random_text(1, 8))
54+
55+
56+
data = [
57+
[
58+
f"2026-01-{(i % 28) + 1:02d}",
59+
rng.choice(WORDS),
60+
random_text(2, 6),
61+
round(rng.random() * 1000, 2),
62+
rng.random() > 0.5,
63+
random_md(),
64+
random_html(),
65+
]
66+
for i in range(ROWS)
67+
]
68+
69+
with gr.Blocks() as demo:
70+
gr.Markdown(
71+
f"### Reproduction for #13279: {ROWS} rows × mixed dtypes (markdown/html/date/number/bool/str)"
72+
)
73+
gr.Dataframe(
74+
value=data, headers=headers, datatype=datatype, interactive=False
75+
)
76+
77+
if __name__ == "__main__":
78+
demo.launch()

js/dataframe/shared/EditableCell.svelte

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,12 @@
136136
handle_blur({ target: { value } } as unknown as FocusEvent);
137137
}
138138
139+
// returning cleanup from the effect fires the blur only when leaving edit mode, not on every render
139140
$effect(() => {
140-
if (!edit) {
141-
// Shim blur on removal for Safari and Firefox
142-
handle_blur({ target: { value } } as unknown as FocusEvent);
141+
if (edit) {
142+
return () => {
143+
handle_blur({ target: { value } } as unknown as FocusEvent);
144+
};
143145
}
144146
});
145147
</script>

js/dataframe/shared/Table.svelte

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { tick, onMount } from "svelte";
1414
import { Upload } from "@gradio/upload";
1515
16+
import { MarkdownCode } from "@gradio/markdown-code";
1617
import HeaderCell from "./HeaderCell.svelte";
1718
import DataCell from "./DataCell.svelte";
1819
import EmptyRowButton from "./EmptyRowButton.svelte";
@@ -263,6 +264,82 @@
263264
return Array.isArray(datatype) ? (datatype[col] ?? "str") : datatype;
264265
}
265266
267+
type SizingEntry = { val: string; col_idx: number; dtype: Datatype };
268+
269+
// heading multipliers for markdown/html block elements that render
270+
// at a larger font size than body text.
271+
const HEADING_MULT = [2.2, 1.7, 1.4, 1.2, 1.1, 1.05];
272+
273+
function md_visual_length(s: string): number {
274+
const heading = s.match(/^\s*(#{1,6})\s+(.+)/);
275+
if (heading) {
276+
const lvl = heading[1].length;
277+
const text = heading[2].replace(/[*_`[\]()]/g, "");
278+
return text.length * HEADING_MULT[lvl - 1];
279+
}
280+
return s.replace(/[*_`#[\]()]/g, "").length;
281+
}
282+
283+
function html_visual_length(s: string): number {
284+
const stripped = s.replace(/<[^>]+>/g, "").length;
285+
const h = s.match(/<h([1-6])\b/i);
286+
if (h) return stripped * HEADING_MULT[parseInt(h[1]) - 1];
287+
return stripped;
288+
}
289+
290+
// mirror EditableCell.truncate_text so the sizing row reserves width
291+
// for the truncated ("…") string, not the full source
292+
function apply_truncation(s: string, dtype: Datatype): string {
293+
if (
294+
max_chars &&
295+
max_chars > 0 &&
296+
dtype !== "image" &&
297+
s.length > max_chars
298+
) {
299+
return s.slice(0, max_chars) + "...";
300+
}
301+
return s;
302+
}
303+
304+
// find the widest rendered value per visible column for the sizing row
305+
function compute_sizing_row(): SizingEntry[] {
306+
const headers = header_groups[0]?.headers ?? [];
307+
return headers.map((header) => {
308+
const col_idx = (header.column.columnDef.meta as any)?.colIndex ?? 0;
309+
const dtype = get_dtype(col_idx);
310+
const accessor = `col_${col_idx}`;
311+
312+
if (dtype === "bool") {
313+
return { val: "false", col_idx, dtype };
314+
}
315+
316+
const visual_len =
317+
dtype === "markdown"
318+
? md_visual_length
319+
: dtype === "html"
320+
? html_visual_length
321+
: (s: string) => s.length;
322+
323+
let best = "";
324+
let best_len = -1;
325+
for (const r of rows) {
326+
const row_idx = r.original._index;
327+
const rendered = editable
328+
? r.original[accessor]
329+
: (display_value?.[row_idx]?.[col_idx] ??
330+
values?.[row_idx]?.[col_idx]);
331+
if (rendered == null) continue;
332+
const v = apply_truncation(String(rendered), dtype);
333+
const len = visual_len(v);
334+
if (len > best_len) {
335+
best_len = len;
336+
best = v;
337+
}
338+
}
339+
return { val: best, col_idx, dtype };
340+
});
341+
}
342+
266343
function get_display_value(row: number, col: number): string {
267344
if (display_value?.[row]?.[col] !== undefined)
268345
return display_value[row][col];
@@ -932,33 +1009,26 @@
9321009

9331010
<tbody class="sizing-body" aria-hidden="true">
9341011
{#if rows.length > 0}
935-
{@const sizing_row = rows.reduce((widest, row) => {
936-
const cells = row.getVisibleCells();
937-
cells.forEach((cell, i) => {
938-
let val = String(cell.getValue() ?? "");
939-
if (
940-
max_chars &&
941-
max_chars > 0 &&
942-
val.length > max_chars &&
943-
get_dtype(i) !== "image"
944-
) {
945-
val = val.slice(0, max_chars) + "...";
946-
}
947-
if (!widest[i] || val.length > widest[i].length) {
948-
widest[i] = val;
949-
}
950-
});
951-
return widest;
952-
}, [] as string[])}
1012+
{@const sizing_row = compute_sizing_row()}
9531013
<tr>
9541014
{#if show_row_numbers}
9551015
<td class="row-number-cell">{rows.length}</td>
9561016
{/if}
957-
{#each sizing_row as val, ci}
958-
{@const dtype = get_dtype(ci)}
1017+
{#each sizing_row as entry (entry.col_idx)}
9591018
<td
9601019
><div class="cell-wrap">
961-
{#if dtype === "html" || dtype === "markdown"}{@html val}{:else}{val}{/if}
1020+
{#if entry.dtype === "markdown"}
1021+
<MarkdownCode
1022+
message={entry.val}
1023+
{latex_delimiters}
1024+
{line_breaks}
1025+
chatbot={false}
1026+
/>
1027+
{:else if entry.dtype === "html"}
1028+
{@html entry.val}
1029+
{:else}
1030+
{entry.val}
1031+
{/if}
9621032
</div></td
9631033
>
9641034
{/each}

0 commit comments

Comments
 (0)