[0.0.10] Shutdown: say what it is waiting on, stop OCR mid-batch, no race with the frame loop
- The exit overlay says which step of shutdown is running and what it waits on ("Stopping the OCR
scan: finishing the text lines it is reading.", the import, the viewer, the models), and how long
that step has taken once it passes 2 s. It said "Saving..." for all of it.
- OCR stops mid-batch: ocr_process takes a cancel flag (Ocr_Engine.cancel), checked by its workers
before each piece of work; a scan sets it to its own cancel. A cut-short batch stores nothing and
counts no failures. Closing during a scan took minutes when a batch held large images; now well
under a second.
- Once shutdown starts, the main thread stops running the app's frames (the viewer, search, import,
OCR, Stats) and draws only the overlay: background_frame read the search thread group while the
shutdown thread was tearing it down, an access violation on exit.
This commit is contained in:
parent
baa188b0e4
commit
775c2873e4
@ -29,7 +29,7 @@ APP_NAME :: "mcap";
|
|||||||
APP_FOLDER_NAME :: "mcap";
|
APP_FOLDER_NAME :: "mcap";
|
||||||
GIT_BRANCH :: "check";
|
GIT_BRANCH :: "check";
|
||||||
GIT_HASH :: "-";
|
GIT_HASH :: "-";
|
||||||
VERSION :: "0.0.9";
|
VERSION :: "0.0.10";
|
||||||
BUILD_DATE :: "-";
|
BUILD_DATE :: "-";
|
||||||
DEBUG :: true;
|
DEBUG :: true;
|
||||||
COMPILE_TESTS :: true;
|
COMPILE_TESTS :: true;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ APP_NAME :: "mcap";
|
|||||||
APP_FOLDER_NAME :: "mcap";
|
APP_FOLDER_NAME :: "mcap";
|
||||||
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
|
APP_BUNDLE_IDENTIFIER :: "com.example.mcap";
|
||||||
|
|
||||||
VERSION :: "0.0.9";
|
VERSION :: "0.0.10";
|
||||||
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
|
// Where everything goes: bin/, or bin-headless/ for builds made with no_crash_dialog, so automated
|
||||||
// builds never write over the ones you run by hand.
|
// builds never write over the ones you run by hand.
|
||||||
build_directory := "bin";
|
build_directory := "bin";
|
||||||
|
|||||||
@ -47,6 +47,16 @@ Ocr_Engine :: struct {
|
|||||||
rec_batch: s32; // text lines per recognizer run: the model's rec_batch_gpu or rec_batch_cpu
|
rec_batch: s32; // text lines per recognizer run: the model's rec_batch_gpu or rec_batch_cpu
|
||||||
options: Ocr_Options;
|
options: Ocr_Options;
|
||||||
stats: Ocr_Stats; // since creation, or the last ocr_reset_stats
|
stats: Ocr_Stats; // since creation, or the last ocr_reset_stats
|
||||||
|
|
||||||
|
// When set, and true, ocr_process stops starting work: the batch of lines or image each worker is
|
||||||
|
// on is finished, and then every image fails as "cancelled" (none is part read). Set by the caller.
|
||||||
|
cancel: *bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
OCR_CANCELLED :: "cancelled";
|
||||||
|
|
||||||
|
ocr_cancelled :: (engine: *Ocr_Engine) -> bool {
|
||||||
|
return engine.cancel && engine.cancel.*;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time spent in each stage, summed over the worker threads (so it can exceed the elapsed time).
|
// Time spent in each stage, summed over the worker threads (so it can exceed the elapsed time).
|
||||||
@ -182,6 +192,7 @@ ocr_process :: (engine: *Ocr_Engine, images: [] Ocr_Image, allocator := context.
|
|||||||
atomic_add(*engine.stats.detect_us, to_microseconds(current_time_monotonic() - start));
|
atomic_add(*engine.stats.detect_us, to_microseconds(current_time_monotonic() - start));
|
||||||
if work.error work.error = copy_string(work.error,, context.default_allocator);
|
if work.error work.error = copy_string(work.error,, context.default_allocator);
|
||||||
}, per_image.data);
|
}, per_image.data);
|
||||||
|
if ocr_cancelled(engine) return ocr_all_cancelled(results, allocator);
|
||||||
|
|
||||||
// Recognition: every line of every image, batched by width so little of each batch is padding.
|
// Recognition: every line of every image, batched by width so little of each batch is padding.
|
||||||
jobs: [..] Line_Job;
|
jobs: [..] Line_Job;
|
||||||
@ -251,6 +262,10 @@ ocr_process :: (engine: *Ocr_Engine, images: [] Ocr_Image, allocator := context.
|
|||||||
atomic_add(*engine.stats.recognize_us, to_microseconds(current_time_monotonic() - start));
|
atomic_add(*engine.stats.recognize_us, to_microseconds(current_time_monotonic() - start));
|
||||||
if !ok { b.failed = true; b.error = copy_string(error,, context.default_allocator); } // the last error wins; any is enough to say why
|
if !ok { b.failed = true; b.error = copy_string(error,, context.default_allocator); } // the last error wins; any is enough to say why
|
||||||
}, *batches);
|
}, *batches);
|
||||||
|
if ocr_cancelled(engine) {
|
||||||
|
free(batches.error,, context.default_allocator);
|
||||||
|
return ocr_all_cancelled(results, allocator);
|
||||||
|
}
|
||||||
if classify.failed && !batches.failed {
|
if classify.failed && !batches.failed {
|
||||||
batches.failed = true;
|
batches.failed = true;
|
||||||
batches.error = copy_string(classify.error,, context.default_allocator);
|
batches.error = copy_string(classify.error,, context.default_allocator);
|
||||||
@ -304,6 +319,14 @@ ocr_process :: (engine: *Ocr_Engine, images: [] Ocr_Image, allocator := context.
|
|||||||
|
|
||||||
#scope_module
|
#scope_module
|
||||||
|
|
||||||
|
ocr_all_cancelled :: (results: [] Ocr_Result, allocator: Allocator) -> [] Ocr_Result {
|
||||||
|
for * results {
|
||||||
|
it.failed = true;
|
||||||
|
it.error = copy_string(OCR_CANCELLED,, allocator);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
Image_Work :: struct {
|
Image_Work :: struct {
|
||||||
image: *Ocr_Image;
|
image: *Ocr_Image;
|
||||||
boxes: [..] Detected_Box;
|
boxes: [..] Detected_Box;
|
||||||
@ -326,6 +349,7 @@ run_on_workers :: (engine: *Ocr_Engine, count: s64, proc: Work_Proc, data: *void
|
|||||||
context.allocator = context.default_allocator;
|
context.allocator = context.default_allocator;
|
||||||
shared := cast(*Shared) thread.data;
|
shared := cast(*Shared) thread.data;
|
||||||
while true {
|
while true {
|
||||||
|
if ocr_cancelled(shared.engine) break; // what is left fails as cancelled (ocr_process)
|
||||||
index := atomic_add(*shared.next, 1);
|
index := atomic_add(*shared.next, 1);
|
||||||
if index >= shared.count break;
|
if index >= shared.count break;
|
||||||
shared.proc(shared.engine, index, shared.data);
|
shared.proc(shared.engine, index, shared.data);
|
||||||
|
|||||||
@ -17,17 +17,73 @@ app_ui_frame_start :: () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Once, on the shutdown thread after the window is asked to close (exit_shutdown.jai): save and
|
// Once, on the shutdown thread after the window is asked to close (exit_shutdown.jai): save and
|
||||||
// stop background work here. Must not touch ImGui, since the main thread is still drawing frames.
|
// stop background work here. Must not touch ImGui, since the main thread is still drawing frames
|
||||||
|
// (the overlay; app_main_window_ui stops running the app's frames once shutdown starts).
|
||||||
app_ui_shutdown :: () {
|
app_ui_shutdown :: () {
|
||||||
|
app_shutdown_step(.IMPORT);
|
||||||
import_shutdown(); // before the viewer: it hands the viewer what it writes
|
import_shutdown(); // before the viewer: it hands the viewer what it writes
|
||||||
|
app_shutdown_step(.VIEWER);
|
||||||
viewer_shutdown();
|
viewer_shutdown();
|
||||||
|
app_shutdown_step(.OCR);
|
||||||
ocr_ui_shutdown();
|
ocr_ui_shutdown();
|
||||||
|
app_shutdown_step(.LABELS);
|
||||||
labels_ui_shutdown(); // after the OCR thread and the background workers have stopped
|
labels_ui_shutdown(); // after the OCR thread and the background workers have stopped
|
||||||
|
app_shutdown_step(.STATS);
|
||||||
stats_ui_shutdown();
|
stats_ui_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which step of app_ui_shutdown is running, and since when, for the overlay a slow shutdown shows.
|
||||||
|
App_Shutdown_Step :: enum s32 { NONE; IMPORT; VIEWER; OCR; LABELS; STATS; }
|
||||||
|
app_shutdown: struct {
|
||||||
|
step: App_Shutdown_Step;
|
||||||
|
step_started_ns: s64; // monotonic
|
||||||
|
}
|
||||||
|
|
||||||
|
app_shutdown_step :: (step: App_Shutdown_Step) {
|
||||||
|
atomic_write(*app_shutdown.step_started_ns, to_nanoseconds(current_time_monotonic()));
|
||||||
|
atomic_write(*app_shutdown.step, step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main thread, while app_ui_shutdown runs: what it is waiting on, as a line or two for the overlay
|
||||||
|
// (temp); "" before it has started a step.
|
||||||
|
app_ui_shutdown_status :: () -> string {
|
||||||
|
step := atomic_read(*app_shutdown.step);
|
||||||
|
text: string;
|
||||||
|
if step == {
|
||||||
|
case .NONE; return "";
|
||||||
|
case .IMPORT;
|
||||||
|
if importer.phase == .INDEXING text = "Stopping the import: it is indexing the library, and stops after the files it is on.";
|
||||||
|
else if importer.running text = "Stopping the import: finishing the files it is on (a large video takes a while to copy).";
|
||||||
|
else text = "Stopping the import.";
|
||||||
|
case .VIEWER;
|
||||||
|
text = "Stopping the thumbnail, catalog and search workers";
|
||||||
|
if viewer.open_started text = tprint("%, and writing out % to play", text, viewer.open_name);
|
||||||
|
text = tprint("%.", text);
|
||||||
|
case .OCR;
|
||||||
|
text = "Closing the OCR model.";
|
||||||
|
if ocr_ui.thread_busy if ocr_ui.task.kind == {
|
||||||
|
case .SCAN; text = "Stopping the OCR scan: finishing the text lines it is reading.";
|
||||||
|
case .ERASE; text = "Stopping the erase: finishing the capture it is on.";
|
||||||
|
case .READ; text = "Waiting for OCR to finish reading a capture.";
|
||||||
|
case .CREATE; text = "Waiting for the OCR model to finish loading.";
|
||||||
|
case .LABELS_CREATE; text = "Waiting for the label model to finish loading.";
|
||||||
|
case .LABEL_SET; text = "Waiting for the label list to finish.";
|
||||||
|
case; text = "Stopping OCR.";
|
||||||
|
}
|
||||||
|
case .LABELS; text = "Closing the label model.";
|
||||||
|
case .STATS; text = "Closing Stats.";
|
||||||
|
}
|
||||||
|
waited := cast(float64) (to_nanoseconds(current_time_monotonic()) - atomic_read(*app_shutdown.step_started_ns)) / 1_000_000_000.0;
|
||||||
|
if waited >= 2 text = tprint("%\n(this step: % s)", text, FormatFloat.{value = waited, trailing_width = 0, zero_removal = .NO});
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
app_main_window_ui :: () {
|
app_main_window_ui :: () {
|
||||||
push_imgui_window(APP_MAIN_WINDOW_LABEL);
|
push_imgui_window(APP_MAIN_WINDOW_LABEL);
|
||||||
|
// Once shutdown has started, its thread is stopping the threads these frames hand work to and take
|
||||||
|
// it from (the viewer's workers, the search group, the import, OCR), and freeing what they share:
|
||||||
|
// running them now would race it. The exit overlay says what it is waiting on.
|
||||||
|
if app_exit_shutdown_started() return;
|
||||||
viewer_ui();
|
viewer_ui();
|
||||||
ocr_ui_frame();
|
ocr_ui_frame();
|
||||||
labels_ui_frame();
|
labels_ui_frame();
|
||||||
|
|||||||
@ -69,12 +69,13 @@ app_exit_shutdown_poll :: () -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For the overlay: what shutdown is doing right now, and how long it has taken so far.
|
// For the overlay: what shutdown is doing right now, and how long it has taken so far; detail, when
|
||||||
app_exit_shutdown_status :: () -> text: string, elapsed_seconds: float64 {
|
// the app says what it is waiting on (app_ui_shutdown_status), is a line or two more. Temp.
|
||||||
|
app_exit_shutdown_status :: () -> text: string, elapsed_seconds: float64, detail: string {
|
||||||
elapsed := to_float64_seconds(app.frame_time - exit_shutdown_started_at);
|
elapsed := to_float64_seconds(app.frame_time - exit_shutdown_started_at);
|
||||||
if atomic_read(*exit_shutdown_stage) == {
|
if atomic_read(*exit_shutdown_stage) == {
|
||||||
case .FINISHING_APP; return "Saving...", elapsed;
|
case .FINISHING_APP; return "Closing...", elapsed, app_ui_shutdown_status();
|
||||||
case; return "Closing...", elapsed;
|
case; return "Closing...", elapsed, "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -200,12 +200,21 @@ ocr_ui_thread_proc :: (thread: *Thread) -> s64 {
|
|||||||
|
|
||||||
case .SCAN;
|
case .SCAN;
|
||||||
scan := *ocr_ui.scan;
|
scan := *ocr_ui.scan;
|
||||||
|
// Cancelling (or closing the app) stops the batch it is on, too, rather than waiting it out:
|
||||||
|
// a batch of large frames can take minutes. What it had not read is left as it was.
|
||||||
|
engine := ocr_ui.engine; // a scan needs one; without, each capture fails as it would
|
||||||
|
if engine engine.cancel = *scan.cancel;
|
||||||
|
defer if engine engine.cancel = null;
|
||||||
position := 0;
|
position := 0;
|
||||||
while position < scan.items.count {
|
while position < scan.items.count {
|
||||||
while scan.paused && !scan.cancel sleep_milliseconds(100);
|
while scan.paused && !scan.cancel sleep_milliseconds(100);
|
||||||
if scan.cancel break;
|
if scan.cancel break;
|
||||||
count := min(OCR_SCAN_BATCH, scan.items.count - position);
|
count := min(OCR_SCAN_BATCH, scan.items.count - position);
|
||||||
errors := ocr_read_and_store(array_view(scan.items, position, count), scan.label_set);
|
errors := ocr_read_and_store(array_view(scan.items, position, count), scan.label_set);
|
||||||
|
if scan.cancel { // the batch was cut short: none of it was stored, and none of it failed
|
||||||
|
for errors free(it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
for errors if it { atomic_add(*scan.failed, 1); log_error("[OCR scan] % %", scan.items[position + it_index].path, it); free(it); }
|
for errors if it { atomic_add(*scan.failed, 1); log_error("[OCR scan] % %", scan.items[position + it_index].path, it); free(it); }
|
||||||
atomic_add(*scan.done, count);
|
atomic_add(*scan.done, count);
|
||||||
position += count;
|
position += count;
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
// A full-viewport overlay shown while app_exit_shutdown_start is running shutdown in the
|
// A full-viewport overlay shown while app_exit_shutdown_start is running shutdown in the
|
||||||
// background (exit_shutdown.jai), so the window stays live and visibly busy instead of looking
|
// background (exit_shutdown.jai), so the window stays live and visibly busy instead of looking
|
||||||
// hung while the app finishes saving.
|
// hung while the app finishes saving, with what it is waiting on.
|
||||||
|
|
||||||
app_exit_shutdown_overlay_ui :: () { using ImGui;
|
app_exit_shutdown_overlay_ui :: () { using ImGui;
|
||||||
if !app_exit_shutdown_started() return;
|
if !app_exit_shutdown_started() return;
|
||||||
|
|
||||||
text, elapsed := app_exit_shutdown_status();
|
text, elapsed, detail := app_exit_shutdown_status();
|
||||||
seconds := FormatFloat.{value = elapsed, trailing_width = 1, zero_removal = .NO};
|
seconds := FormatFloat.{value = elapsed, trailing_width = 1, zero_removal = .NO};
|
||||||
label := tprint("%1 (%2 s)", text, seconds);
|
label := tprint("%1 (%2 s)", text, seconds);
|
||||||
|
if detail label = tprint("%\n%", label, detail); // what it is waiting on
|
||||||
|
|
||||||
viewport := GetMainViewport();
|
viewport := GetMainViewport();
|
||||||
draw_list := GetForegroundDrawList(viewport);
|
draw_list := GetForegroundDrawList(viewport);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user