overhead investigation + optimization
Browse files- server.py +21 -4
- static/app.js +19 -2
server.py
CHANGED
|
@@ -215,6 +215,13 @@ def multlist(bit_count: int = 2, min: int = -10, max: int = 20, mode: str = "Sta
|
|
| 215 |
|
| 216 |
@app.post("/api/compress")
|
| 217 |
async def compress(request: Request):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
form = await request.form()
|
| 219 |
upload = form.get("image")
|
| 220 |
if upload is None:
|
|
@@ -225,6 +232,7 @@ async def compress(request: Request):
|
|
| 225 |
except Exception as exc:
|
| 226 |
return JSONResponse({"error": f"Could not read image: {exc}"}, status_code=400)
|
| 227 |
w, h = img.size
|
|
|
|
| 228 |
|
| 229 |
mode = form.get("mode", "Auto")
|
| 230 |
if mode == "Auto":
|
|
@@ -242,24 +250,33 @@ async def compress(request: Request):
|
|
| 242 |
except (ValueError, TypeError):
|
| 243 |
pass
|
| 244 |
config = PBC3Config(**kwargs)
|
|
|
|
| 245 |
|
| 246 |
try:
|
| 247 |
result = PBC3.compress(img, config=config)
|
| 248 |
except Exception as exc:
|
| 249 |
return JSONResponse({"error": f"Compression failed: {exc}"}, status_code=400)
|
| 250 |
reconstructed = result.image.convert("RGB")
|
|
|
|
| 251 |
|
| 252 |
a = np.asarray(img, dtype=np.float32)
|
| 253 |
b = np.asarray(reconstructed.resize(img.size), dtype=np.float32)
|
| 254 |
mse = mse_metric(a, b)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
original_raw = w * h * 3
|
| 257 |
compressed = len(result.data)
|
|
|
|
| 258 |
|
| 259 |
return JSONResponse({
|
| 260 |
-
"
|
| 261 |
-
"
|
| 262 |
-
"pbc_base64": base64.b64encode(result.data).decode(),
|
| 263 |
"width": w,
|
| 264 |
"height": h,
|
| 265 |
"original_raw_kb": round(original_raw / 1024, 2),
|
|
@@ -269,10 +286,10 @@ async def compress(request: Request):
|
|
| 269 |
"compression_percent": round(compressed / original_raw * 100, 2) if original_raw else 0,
|
| 270 |
"mse": round(mse, 2),
|
| 271 |
"time_seconds": round(result.encode_seconds, 2),
|
|
|
|
| 272 |
"params": {"mode": mode, **{k: (list(v) if isinstance(v, tuple) else v) for k, v in kwargs.items()}},
|
| 273 |
})
|
| 274 |
|
| 275 |
-
|
| 276 |
@app.post("/api/stream_compress")
|
| 277 |
async def stream_compress(image: UploadFile = File(...), downsample_initialize: str = Form("false")):
|
| 278 |
raw = await image.read()
|
|
|
|
| 215 |
|
| 216 |
@app.post("/api/compress")
|
| 217 |
async def compress(request: Request):
|
| 218 |
+
stages = {}
|
| 219 |
+
t = time.perf_counter()
|
| 220 |
+
def mark(name):
|
| 221 |
+
nonlocal t
|
| 222 |
+
stages[name] = round(time.perf_counter() - t, 3)
|
| 223 |
+
t = time.perf_counter()
|
| 224 |
+
|
| 225 |
form = await request.form()
|
| 226 |
upload = form.get("image")
|
| 227 |
if upload is None:
|
|
|
|
| 232 |
except Exception as exc:
|
| 233 |
return JSONResponse({"error": f"Could not read image: {exc}"}, status_code=400)
|
| 234 |
w, h = img.size
|
| 235 |
+
mark("read_decode")
|
| 236 |
|
| 237 |
mode = form.get("mode", "Auto")
|
| 238 |
if mode == "Auto":
|
|
|
|
| 250 |
except (ValueError, TypeError):
|
| 251 |
pass
|
| 252 |
config = PBC3Config(**kwargs)
|
| 253 |
+
mark("build_config")
|
| 254 |
|
| 255 |
try:
|
| 256 |
result = PBC3.compress(img, config=config)
|
| 257 |
except Exception as exc:
|
| 258 |
return JSONResponse({"error": f"Compression failed: {exc}"}, status_code=400)
|
| 259 |
reconstructed = result.image.convert("RGB")
|
| 260 |
+
mark("compress")
|
| 261 |
|
| 262 |
a = np.asarray(img, dtype=np.float32)
|
| 263 |
b = np.asarray(reconstructed.resize(img.size), dtype=np.float32)
|
| 264 |
mse = mse_metric(a, b)
|
| 265 |
+
mark("mse")
|
| 266 |
+
|
| 267 |
+
recon_b64 = _png_b64(reconstructed) # original is now sent by the client (it has the bytes)
|
| 268 |
+
mark("png_reconstructed")
|
| 269 |
+
|
| 270 |
+
pbc_b64 = base64.b64encode(result.data).decode()
|
| 271 |
+
mark("encode_payload")
|
| 272 |
|
| 273 |
original_raw = w * h * 3
|
| 274 |
compressed = len(result.data)
|
| 275 |
+
print(f"[compress] encode={result.encode_seconds:.3f}s stages={stages}", flush=True)
|
| 276 |
|
| 277 |
return JSONResponse({
|
| 278 |
+
"reconstructed_image": recon_b64,
|
| 279 |
+
"pbc_base64": pbc_b64,
|
|
|
|
| 280 |
"width": w,
|
| 281 |
"height": h,
|
| 282 |
"original_raw_kb": round(original_raw / 1024, 2),
|
|
|
|
| 286 |
"compression_percent": round(compressed / original_raw * 100, 2) if original_raw else 0,
|
| 287 |
"mse": round(mse, 2),
|
| 288 |
"time_seconds": round(result.encode_seconds, 2),
|
| 289 |
+
"stage_timings": stages,
|
| 290 |
"params": {"mode": mode, **{k: (list(v) if isinstance(v, tuple) else v) for k, v in kwargs.items()}},
|
| 291 |
})
|
| 292 |
|
|
|
|
| 293 |
@app.post("/api/stream_compress")
|
| 294 |
async def stream_compress(image: UploadFile = File(...), downsample_initialize: str = Form("false")):
|
| 295 |
raw = await image.read()
|
static/app.js
CHANGED
|
@@ -604,7 +604,16 @@ function renderJob(job) {
|
|
| 604 |
function updateJobMeta(job) {
|
| 605 |
const m = job.el.querySelector(".job-meta");
|
| 606 |
if (job.status === "sent") m.textContent = `${((performance.now() - job.tSent) / 1000).toFixed(1)}s`;
|
| 607 |
-
else if (job.status === "done")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
else if (job.status === "error") m.textContent = "failed — see console";
|
| 609 |
else m.textContent = "waiting…";
|
| 610 |
}
|
|
@@ -628,7 +637,7 @@ function scheduleJobRemoval(job) {
|
|
| 628 |
}
|
| 629 |
|
| 630 |
function enqueueJob(file) {
|
| 631 |
-
const job = { name: file.name, fd: buildCompressForm(file), status: "queued" };
|
| 632 |
job.el = renderJob(job);
|
| 633 |
jobQueueEl.appendChild(job.el);
|
| 634 |
jobQueue.push(job);
|
|
@@ -657,6 +666,14 @@ async function runJob(job) {
|
|
| 657 |
const data = await res.json();
|
| 658 |
job.total = (performance.now() - job.tSent) / 1000;
|
| 659 |
job.encode = data.time_seconds;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
setJobState(job, "done");
|
| 661 |
addToRegistry(data);
|
| 662 |
showLastResult(data);
|
|
|
|
| 604 |
function updateJobMeta(job) {
|
| 605 |
const m = job.el.querySelector(".job-meta");
|
| 606 |
if (job.status === "sent") m.textContent = `${((performance.now() - job.tSent) / 1000).toFixed(1)}s`;
|
| 607 |
+
else if (job.status === "done") {
|
| 608 |
+
let s = `total ${job.total.toFixed(1)}s · encode ${job.encode.toFixed(2)}s · overhead ${Math.max(0, job.total - job.encode).toFixed(1)}s`;
|
| 609 |
+
if (job.stages) {
|
| 610 |
+
const top = Object.entries(job.stages).sort((a, b) => b[1] - a[1]).slice(0, 3)
|
| 611 |
+
.map(([k, v]) => `${k} ${v}s`).join(" · ");
|
| 612 |
+
s += `<br><span style="opacity:.65">server: ${top}</span>`;
|
| 613 |
+
}
|
| 614 |
+
job.el.querySelector(".job-meta").innerHTML = s;
|
| 615 |
+
return;
|
| 616 |
+
}
|
| 617 |
else if (job.status === "error") m.textContent = "failed — see console";
|
| 618 |
else m.textContent = "waiting…";
|
| 619 |
}
|
|
|
|
| 637 |
}
|
| 638 |
|
| 639 |
function enqueueJob(file) {
|
| 640 |
+
const job = { name: file.name, file, fd: buildCompressForm(file), status: "queued" };
|
| 641 |
job.el = renderJob(job);
|
| 642 |
jobQueueEl.appendChild(job.el);
|
| 643 |
jobQueue.push(job);
|
|
|
|
| 666 |
const data = await res.json();
|
| 667 |
job.total = (performance.now() - job.tSent) / 1000;
|
| 668 |
job.encode = data.time_seconds;
|
| 669 |
+
job.stages = data.stage_timings;
|
| 670 |
+
if (job.file && !data.decoded && !data.original_image) {
|
| 671 |
+
data.original_image = await new Promise(r => {
|
| 672 |
+
const fr = new FileReader();
|
| 673 |
+
fr.onload = () => r(fr.result);
|
| 674 |
+
fr.readAsDataURL(job.file);
|
| 675 |
+
});
|
| 676 |
+
}
|
| 677 |
setJobState(job, "done");
|
| 678 |
addToRegistry(data);
|
| 679 |
showLastResult(data);
|