Skip to content

Commit 60e6b3e

Browse files
authored
Show progress bars when downloading assets, update README (#257)
* Display download progress for remotely hosted assets * Fix race condition between inputs and available options, avoiding duplicate downloads of the same asset * Show logs for download progress * Update README * Bump app version
1 parent 25ab4a9 commit 60e6b3e

File tree

6 files changed

+227
-7
lines changed

6 files changed

+227
-7
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,23 @@ and diagnostic plots from the individual analysis steps.
7070
- Clicking on "What's happening" will show logs describing how long each step of the analysis took (and any errors during the analysis).
7171
- Clicking Export will save the analysis either to the browser or download the analysis as a .kana file. Loading these files will restore the state of the application
7272

73+
If you use **Kana** for analysis or exploration, consider citing our JOSS publication -
74+
75+
```bibtex
76+
@article{Kana2023,
77+
doi = {10.21105/joss.05603},
78+
url = {https://doi.org/10.21105/joss.05603},
79+
year = {2023},
80+
publisher = {The Open Journal},
81+
volume = {8},
82+
number = {89},
83+
pages = {5603},
84+
author = {Aaron Tin Long Lun and Jayaram Kancherla},
85+
title = {Powering single-cell analyses in the browser with WebAssembly},
86+
journal = {Journal of Open Source Software}
87+
}
88+
```
89+
7390
## For developers
7491

7592
***Check out [Contributing](./CONTRIBUTING.md) for guidelines on opening issues and pull requests.***

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "kana",
33
"description": "Single-cell data analysis in the browser",
4-
"version": "3.0.22",
4+
"version": "3.0.23",
55
"author": {
66
"name": "Jayaram Kancherla",
77
"email": "[email protected]",

src/DownloadToaster.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {
2+
OverlayToaster,
3+
Position,
4+
ProgressBar,
5+
Classes,
6+
} from "@blueprintjs/core";
7+
8+
import { Tooltip2 } from "@blueprintjs/popover2";
9+
10+
import classNames from "classnames";
11+
12+
export const DownloadToaster = OverlayToaster.create({
13+
className: "recipe-toaster",
14+
position: Position.TOP_RIGHT,
15+
});
16+
17+
let download_toasters = {};
18+
19+
export function setProgress(id, total, progress) {
20+
if (total !== null) {
21+
download_toasters["total"] = total;
22+
download_toasters["progress"] = progress;
23+
}
24+
25+
if (progress !== null) {
26+
let tprogress =
27+
(Math.round((progress * 100) / download_toasters["total"]) / 100) * 100;
28+
29+
download_toasters["progress"] = tprogress;
30+
}
31+
}
32+
33+
export function renderProgress(progress, url) {
34+
return {
35+
icon: "cloud-download",
36+
message: (
37+
<>
38+
<>
39+
Downloading asset from{" "}
40+
<Tooltip2
41+
className={Classes.TOOLTIP_INDICATOR}
42+
content={<span>{url}</span>}
43+
minimal={true}
44+
usePortal={false}
45+
>
46+
{new URL(url).hostname}
47+
</Tooltip2>
48+
</>
49+
<ProgressBar
50+
className={classNames("docs-toast-progress", {
51+
[Classes.PROGRESS_NO_STRIPES]: progress >= 100,
52+
})}
53+
intent={progress < 100 ? "primary" : "success"}
54+
value={progress / 100}
55+
/>
56+
</>
57+
),
58+
timeout: progress < 100 ? 0 : 1000,
59+
};
60+
}

src/components/AnalysisMode/index.js

+62-3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import Gallery from "../Gallery/index";
4242

4343
import { AppToaster } from "../../AppToaster";
4444

45+
import { renderProgress, DownloadToaster } from "../../DownloadToaster";
46+
4547
import { AppContext } from "../../context/AppContext";
4648

4749
import pkgVersion from "../../../package.json";
@@ -59,6 +61,8 @@ const scranWorker = new Worker(
5961

6062
let logs = [];
6163

64+
let download_toasters = {};
65+
6266
export function AnalysisMode(props) {
6367
// true until wasm is initialized
6468
const [loading, setLoading] = useState(true);
@@ -135,6 +139,8 @@ export function AnalysisMode(props) {
135139
const [initDims, setInitDims] = useState(null);
136140
const [inputData, setInputData] = useState(null);
137141

142+
const [preInputDone, setPreInputDone] = useState(false);
143+
138144
// STEP: QC; for all three, RNA, ADT, CRISPR
139145
// dim sizes
140146
const [qcDims, setQcDims] = useState(null);
@@ -307,6 +313,7 @@ export function AnalysisMode(props) {
307313
useEffect(() => {
308314
if (wasmInitialized && preInputFiles) {
309315
if (preInputFiles.files) {
316+
setPreInputDone(false);
310317
scranWorker.postMessage({
311318
type: "PREFLIGHT_INPUT",
312319
payload: {
@@ -318,7 +325,7 @@ export function AnalysisMode(props) {
318325
}, [preInputFiles, wasmInitialized]);
319326

320327
useEffect(() => {
321-
if (wasmInitialized && preInputFiles) {
328+
if (wasmInitialized && preInputFiles && preInputDone && preInputOptions) {
322329
if (preInputFiles.files && preInputOptions.options.length > 0) {
323330
scranWorker.postMessage({
324331
type: "PREFLIGHT_OPTIONS",
@@ -329,7 +336,7 @@ export function AnalysisMode(props) {
329336
});
330337
}
331338
}
332-
}, [preInputOptions, wasmInitialized]);
339+
}, [preInputOptions, wasmInitialized, preInputDone]);
333340

334341
// NEW analysis: files are imported into Kana
335342
useEffect(() => {
@@ -854,7 +861,7 @@ export function AnalysisMode(props) {
854861
scranWorker.onmessage = (msg) => {
855862
const payload = msg.data;
856863

857-
// console.log("ON MAIN::RCV::", payload);
864+
// console.log("IN ANALYSIS MODE, ON MAIN::RCV::", payload);
858865

859866
// process any error messages
860867
if (payload) {
@@ -904,6 +911,57 @@ export function AnalysisMode(props) {
904911
}
905912
}
906913

914+
if (payload.type.startsWith("DOWNLOAD")) {
915+
if (payload.download == "START") {
916+
download_toasters[payload.url] = {
917+
total: payload.total_bytes,
918+
progress: 0,
919+
};
920+
download_toasters[payload.url]["key"] = DownloadToaster.show(
921+
renderProgress(0, payload.url)
922+
);
923+
924+
add_to_logs("start", `Download asset from ${payload.url}`, "started");
925+
} else if (payload.download == "PROGRESS") {
926+
let tprogress =
927+
(Math.round(
928+
(payload.downloaded_bytes * 100) /
929+
download_toasters[payload.url]["total"]
930+
) /
931+
100) *
932+
100;
933+
934+
if (tprogress < 100) {
935+
download_toasters[payload.url]["progress"] = tprogress;
936+
937+
download_toasters[payload.url]["key"] = DownloadToaster.show(
938+
renderProgress(tprogress, payload.url),
939+
download_toasters[payload.url]["key"]
940+
);
941+
}
942+
add_to_logs("progress", `Downloading ${tprogress}% done`, "");
943+
} else if (payload.download == "COMPLETE") {
944+
download_toasters[payload.url]["progress"] = 100;
945+
946+
download_toasters[payload.url]["key"] = DownloadToaster.show(
947+
renderProgress(100, payload.url),
948+
download_toasters[payload.url]["key"]
949+
);
950+
951+
add_to_logs(
952+
"complete",
953+
`Asset downloaded from ${payload.url}`,
954+
"finished"
955+
);
956+
957+
setTimeout(() => {
958+
delete download_toasters[payload.url];
959+
}, 500);
960+
}
961+
962+
return;
963+
}
964+
907965
const { resp, type } = payload;
908966

909967
if (type === "INIT") {
@@ -927,6 +985,7 @@ export function AnalysisMode(props) {
927985
} else if (type === "PREFLIGHT_INPUT_DATA") {
928986
if (resp.details) {
929987
setPreInputFilesStatus(resp.details);
988+
setPreInputDone(true);
930989
}
931990
} else if (type === "PREFLIGHT_OPTIONS_DATA") {
932991
if (resp) {

src/workers/helpers.js

+85-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,91 @@ import * as downloads from "./DownloadsDBHandler.js";
66
// Evade CORS problems and enable caching.
77
const proxy = "https://cors-proxy.aaron-lun.workers.dev";
88
async function proxyAndCache(url) {
9-
let buffer = await downloads.get(proxy + "/" + encodeURIComponent(url));
10-
return new Uint8Array(buffer);
9+
const url_with_proxy = proxy + "/" + encodeURIComponent(url);
10+
11+
try {
12+
const out = await fetchWithProgress(
13+
url_with_proxy,
14+
(cl) => {
15+
postMessage({
16+
type: `DOWNLOAD for url: ` + String(url),
17+
download: "START",
18+
url: String(url),
19+
total_bytes: String(cl),
20+
msg: "Total size is " + String(cl) + " bytes!",
21+
});
22+
return url_with_proxy;
23+
},
24+
(id, sofar) => {
25+
postMessage({
26+
type: `DOWNLOAD for url: ` + String(url),
27+
download: "PROGRESS",
28+
url: String(url),
29+
downloaded_bytes: String(sofar),
30+
msg: "Progress so far, got " + String(sofar) + " bytes!",
31+
});
32+
},
33+
(id, total) => {
34+
postMessage({
35+
type: `DOWNLOAD for url: ` + String(url),
36+
download: "COMPLETE",
37+
url: String(url),
38+
msg: "Finished, got " + String(total) + " bytes!",
39+
});
40+
}
41+
);
42+
43+
return out;
44+
} catch (error) {
45+
// console.log("oops error", error)
46+
postMessage({
47+
type: `DOWNLOAD for url: ` + String(url),
48+
download: "START",
49+
url: String(url),
50+
total_bytes: 100,
51+
});
52+
let buffer = await downloads.get(url_with_proxy);
53+
postMessage({
54+
type: `DOWNLOAD for url: ` + String(url),
55+
download: "COMPLETE",
56+
url: String(url),
57+
});
58+
return new Uint8Array(buffer);
59+
}
60+
}
61+
62+
async function fetchWithProgress(url, startFun, iterFun, endFun) {
63+
const res = await fetch(url);
64+
if (!res.ok) {
65+
throw new Error("oops, failed to download '" + url + "'");
66+
}
67+
68+
const cl = res.headers.get("content-length"); // WARNING: this might be NULL!
69+
const id = startFun(cl);
70+
71+
const reader = res.body.getReader();
72+
const chunks = [];
73+
let total = 0;
74+
75+
while (true) {
76+
const { done, value } = await reader.read();
77+
if (done) {
78+
break;
79+
}
80+
chunks.push(value);
81+
total += value.length;
82+
iterFun(id, total);
83+
}
84+
85+
let output = new Uint8Array(total);
86+
let start = 0;
87+
for (const x of chunks) {
88+
output.set(x, start);
89+
start += x.length;
90+
}
91+
92+
endFun(id, total);
93+
return output;
1194
}
1295

1396
bakana.CellLabellingState.setDownload(proxyAndCache);

src/workers/scran.worker.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
fetchStepSummary,
1717
describeColumn,
1818
isArrayOrView,
19+
fetchWithProgress,
1920
} from "./helpers.js";
2021
import { code } from "../utils/utils.js";
2122
/***************************************/
@@ -306,7 +307,7 @@ var loaded;
306307
onmessage = function (msg) {
307308
const { type, payload } = msg.data;
308309

309-
// console.log("WORKER::RCV::", type, payload);
310+
// console.log("SCRAN.WORKER ::RCV::", type, payload);
310311

311312
let fatal = false;
312313
if (type === "INIT") {

0 commit comments

Comments
 (0)