Skip to content

Commit d41c0a9

Browse files
drorganvidezclaude
andcommitted
perf(dataset): defer UVL load to async fetch on DOI view
The DOI landing page used to read the first UVL file from disk synchronously and embed its full contents into the rendered HTML, which made 20 MB datasets take ~5 minutes to render. Page now ships empty with both skeletons visible, fetches the file via the existing /hubfiles/<id>/workbench-content endpoint after DOMContentLoaded, and applies it to CodeMirror without syntax highlighting when the file is over 1 MB. Also drops viewportMargin: Infinity, which was forcing CodeMirror to render every line of large files at once. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 49f3c8c commit d41c0a9

3 files changed

Lines changed: 44 additions & 54 deletions

File tree

app/modules/dataset/routes.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ def subdomain_index(doi):
562562

563563
# HTML fallback (default for browsers and crawlers)
564564
hubfiles = [file for fm in dataset.feature_models for file in fm.hubfiles]
565-
selected_file, uvl_content = _preload_first_file(dataset, hubfiles)
565+
selected_file = hubfiles[0] if hubfiles else None
566566

567567
user_cookie = ds_view_record_service.create_cookie(dataset=dataset)
568568

@@ -587,7 +587,7 @@ def subdomain_index(doi):
587587
dataset=dataset,
588588
hubfiles=hubfiles,
589589
selected_file=selected_file,
590-
uvl_content=uvl_content,
590+
uvl_content=None,
591591
fair_meta=fair_meta,
592592
)
593593
)
@@ -597,20 +597,6 @@ def subdomain_index(doi):
597597
return resp
598598

599599

600-
def _preload_first_file(dataset, hubfiles):
601-
"""Pick the first hubfile (if any) and return (file, uvl_content)."""
602-
if not hubfiles:
603-
return None, None
604-
first = hubfiles[0]
605-
directory_path = os.path.join("uploads", f"user_{dataset.user_id}", f"dataset_{dataset.id}", "uvl")
606-
file_path = os.path.join(current_app.root_path, "..", directory_path, first.name)
607-
try:
608-
with open(file_path, "r") as f:
609-
return first, f.read()
610-
except Exception as e:
611-
return first, f"[Error reading file: {e}]"
612-
613-
614600
@dataset_bp.route("/doi/<path:doi>/files/raw/<path:filename>", methods=["GET"])
615601
@dataset_bp.route("/doi/<path:doi>/files/raw/<path:filename>/", methods=["GET"])
616602
def doi_file_raw(doi, filename):
@@ -670,14 +656,14 @@ def get_unsynchronized_dataset(dataset_id):
670656
abort(404)
671657

672658
hubfiles = [file for fm in dataset.feature_models for file in fm.hubfiles]
673-
selected_file, uvl_content = _preload_first_file(dataset, hubfiles)
659+
selected_file = hubfiles[0] if hubfiles else None
674660

675661
return render_template(
676662
"dataset/view_dataset.html",
677663
dataset=dataset,
678664
hubfiles=hubfiles,
679665
selected_file=selected_file,
680-
uvl_content=uvl_content,
666+
uvl_content=None,
681667
)
682668

683669

app/modules/dataset/templates/dataset/view_dataset.html

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ <h5 class="mb-2">No models in this dataset yet</h5>
307307
</div>
308308
<div class="card-body p-0 panel-body position-relative">
309309
<textarea id="uvlEditor" class="d-none">{% if uvl_content %}{{ uvl_content }}{% endif %}</textarea>
310-
<div id="uvlSkeleton" class="uvl-skeleton d-none p-4">
310+
<div id="uvlSkeleton" class="uvl-skeleton {% if not selected_file %}d-none {% endif %}p-4">
311311
<span class="skeleton skeleton-line" style="width: 25%;"></span>
312312
<span class="skeleton skeleton-line" style="width: 55%; margin-left: 1rem;"></span>
313313
<span class="skeleton skeleton-line" style="width: 40%; margin-left: 2rem;"></span>
@@ -380,7 +380,7 @@ <h5 class="mb-2">No models in this dataset yet</h5>
380380
<div class="card-body panel-body">
381381
<div id="FMFactLabelWrapper" class="w-100 position-relative">
382382
<svg id="FMFactLabelChart" class="chart w-100" preserveAspectRatio="xMinYMin meet"></svg>
383-
<div id="factLabelSkeleton" class="factlabel-skeleton d-none">
383+
<div id="factLabelSkeleton" class="factlabel-skeleton {% if not selected_file %}d-none{% endif %}">
384384
<span class="skeleton skeleton-line xl mb-3" style="width: 50%;"></span>
385385
<span class="skeleton skeleton-line" style="width: 80%;"></span>
386386
<span class="skeleton skeleton-line" style="width: 65%;"></span>
@@ -617,6 +617,12 @@ <h5 class="fw-bold text-primary mb-0" id="metricModalLabel">Metric Details</h5>
617617
.uvl-skeleton { min-height: 360px; }
618618
.uvl-skeleton .skeleton-line { font-family: monospace; }
619619
.workbench-panels .CodeMirror.is-loading { visibility: hidden; }
620+
/* Overlay the UVL skeleton on top of the (hidden) CodeMirror so the
621+
panel doesn't double its height while the file is loading. */
622+
.panel-section[data-panel-key="uvl"] .panel-body { min-height: 360px; }
623+
.panel-section[data-panel-key="uvl"] #uvlSkeleton {
624+
position: absolute; inset: 0; z-index: 2;
625+
}
620626
</style>
621627

622628
<script>
@@ -675,13 +681,15 @@ <h5 class="fw-bold text-primary mb-0" id="metricModalLabel">Metric Details</h5>
675681
initDescriptionToggle();
676682
if (currentFileId) {
677683
applyFileUi(findModel(currentFileId));
678-
renderFactLabelForCurrentFile();
684+
fetchAndApplyContent(currentFileId);
679685
}
680686
}
681687
initTooltips();
682688
});
683689

684690
// ---------- Editor ----------
691+
const UVL_PLAIN_THRESHOLD_BYTES = 1024 * 1024; // 1 MB: above this, skip syntax highlighting
692+
685693
function initUvlEditor() {
686694
const ta = document.getElementById('uvlEditor');
687695
if (!ta || !window.CodeMirror) return;
@@ -694,9 +702,32 @@ <h5 class="fw-bold text-primary mb-0" id="metricModalLabel">Metric Details</h5>
694702
indentUnit: 2,
695703
lineWrapping: false,
696704
foldGutter: true,
697-
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
698-
viewportMargin: Infinity
705+
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]
699706
});
707+
// Hide CodeMirror until the async fetch lands content; the skeleton
708+
// (rendered visible by the template) carries the visual.
709+
if (currentFileId) setUvlLoading(true);
710+
}
711+
712+
function applyUvlContent(text) {
713+
if (!uvlEditorInstance) {
714+
setUvlLoading(false);
715+
return;
716+
}
717+
const big = (text || '').length > UVL_PLAIN_THRESHOLD_BYTES;
718+
uvlEditorInstance.setOption('mode', big ? null : 'uvl');
719+
// Defer setValue so the loading skeleton paints first; CodeMirror's
720+
// setValue can block the main thread for several seconds on large files.
721+
setTimeout(() => {
722+
try {
723+
if (uvlEditorInstance) {
724+
uvlEditorInstance.setValue(text || '');
725+
uvlEditorInstance.refresh();
726+
}
727+
} finally {
728+
setUvlLoading(false);
729+
}
730+
}, 0);
700731
}
701732

702733
// ---------- Panel collapsing ----------
@@ -954,11 +985,7 @@ <h5 class="fw-bold text-primary mb-0" id="metricModalLabel">Metric Details</h5>
954985
fetch(url, { credentials: 'same-origin' })
955986
.then(r => r.ok ? r.json() : Promise.reject(r.status))
956987
.then(data => {
957-
if (uvlEditorInstance) {
958-
uvlEditorInstance.setValue(data.uvl || '');
959-
uvlEditorInstance.refresh();
960-
setTimeout(() => uvlEditorInstance.refresh(), 0);
961-
}
988+
applyUvlContent(data.uvl || '');
962989
const ideLink = document.getElementById('workbench-ide-link');
963990
if (ideLink) ideLink.href = data.ide_url;
964991
const fl = document.getElementById('workbench-factlabel-external');
@@ -967,11 +994,8 @@ <h5 class="fw-bold text-primary mb-0" id="metricModalLabel">Metric Details</h5>
967994
})
968995
.catch(err => {
969996
console.error('workbench fetch error', err);
970-
if (uvlEditorInstance) uvlEditorInstance.setValue(`[Error loading model: ${err}]`);
997+
applyUvlContent(`[Error loading model: ${err}]`);
971998
setFactLabelLoading(false);
972-
})
973-
.finally(() => {
974-
setUvlLoading(false);
975999
});
9761000
}
9771001

app/modules/hubfile/routes.py

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -152,24 +152,14 @@ def view_unsynchronized_file(dataset_id, file_id):
152152
if not dataset or not selected_file:
153153
abort(404)
154154

155-
# 4. Build the on-disk path to the file.
156-
directory_path = os.path.join("uploads", f"user_{dataset.user_id}", f"dataset_{dataset.id}", "uvl")
157-
file_path = os.path.join(current_app.root_path, "..", directory_path, selected_file.name)
158-
159-
try:
160-
with open(file_path, "r") as f:
161-
content = f.read()
162-
except Exception as e:
163-
content = f"[Error reading file: {e}]"
164-
165155
user_cookie = hubfile_view_record_service.create_cookie(hubfile=selected_file)
166156
resp = make_response(
167157
render_template(
168158
"dataset/view_dataset.html",
169159
selected_file=selected_file,
170160
hubfiles=dataset.files(),
171161
dataset=dataset,
172-
uvl_content=content,
162+
uvl_content=None,
173163
)
174164
)
175165
resp.set_cookie("file_view_cookie", user_cookie)
@@ -201,24 +191,14 @@ def view_uvl_with_doi(doi, filename):
201191
if not selected_file:
202192
abort(404)
203193

204-
# 4. Build the on-disk path to the file.
205-
directory_path = os.path.join("uploads", f"user_{dataset.user_id}", f"dataset_{dataset.id}", "uvl")
206-
file_path = os.path.join(current_app.root_path, "..", directory_path, selected_file.name)
207-
208-
try:
209-
with open(file_path, "r") as f:
210-
content = f.read()
211-
except Exception as e:
212-
content = f"[Error reading file: {e}]"
213-
214194
user_cookie = hubfile_view_record_service.create_cookie(hubfile=selected_file)
215195
resp = make_response(
216196
render_template(
217197
"dataset/view_dataset.html",
218198
selected_file=selected_file,
219199
hubfiles=dataset.files(),
220200
dataset=dataset,
221-
uvl_content=content,
201+
uvl_content=None,
222202
)
223203
)
224204
resp.set_cookie("file_view_cookie", user_cookie)

0 commit comments

Comments
 (0)