[0.0.4] Search and Stats on background threads, so typing never stalls the UI
This commit is contained in:
parent
8c9870ac01
commit
3c6da5b53d
@ -37,7 +37,9 @@ of previews, and one capture at full size in the Capture window.
|
|||||||
- **Search** above the grid: every word must match, anywhere in the OCR text, window title, URL, app
|
- **Search** above the grid: every word must match, anywhere in the OCR text, window title, URL, app
|
||||||
name or annotation text; `"a phrase"` matches as one; `ocr:`, `title:`, `url:`, `app:` and `note:`
|
name or annotation text; `"a phrase"` matches as one; `ocr:`, `title:`, `url:`, `app:` and `note:`
|
||||||
limit a word to one field. In the Capture window the matching OCR lines are highlighted, with Next
|
limit a word to one field. In the Capture window the matching OCR lines are highlighted, with Next
|
||||||
and Previous.
|
and Previous. Matching runs on a pool of background threads (`app/background.jai`), as do the
|
||||||
|
Stats window's numbers, so typing never waits on them; the grid shows the last results until the
|
||||||
|
new ones are back, usually within a frame.
|
||||||
- **Order:** newest first by capture date, once the catalog (what search needs from every file) has
|
- **Order:** newest first by capture date, once the catalog (what search needs from every file) has
|
||||||
loaded; by name until then.
|
loaded; by name until then.
|
||||||
- **OCR** (`modules/OCR`): the OCR window shows whether it runs on the GPU and, if not, what to install,
|
- **OCR** (`modules/OCR`): the OCR window shows whether it runs on the GPU and, if not, what to install,
|
||||||
|
|||||||
@ -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.3";
|
VERSION :: "0.0.4";
|
||||||
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.3";
|
VERSION :: "0.0.4";
|
||||||
// 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";
|
||||||
|
|||||||
@ -19,6 +19,7 @@ USING_WINDOWS :: OS == .WINDOWS;
|
|||||||
#load "app/ocr_ui.jai";
|
#load "app/ocr_ui.jai";
|
||||||
#load "app/catalog.jai";
|
#load "app/catalog.jai";
|
||||||
#load "app/stats_ui.jai";
|
#load "app/stats_ui.jai";
|
||||||
|
#load "app/background.jai";
|
||||||
#load "app/exit_shutdown.jai";
|
#load "app/exit_shutdown.jai";
|
||||||
|
|
||||||
#load "base/circle_buffer.jai";
|
#load "base/circle_buffer.jai";
|
||||||
|
|||||||
259
src/app/background.jai
Normal file
259
src/app/background.jai
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
// Background work for the viewer, on a thread group: matching the search against the catalog, and
|
||||||
|
// the Statistics window's numbers. The UI thread hands out jobs and takes back results each frame;
|
||||||
|
// it never waits on them, so typing in the search box or opening Stats cannot stall a frame. Until
|
||||||
|
// a search's results are back, the grid keeps showing the last ones.
|
||||||
|
//
|
||||||
|
// A job gets a snapshot (capture index, catalog entry, name) of the captures it covers, made on the
|
||||||
|
// UI thread, and reads nothing else: never viewer.captures, which a refresh can grow. Catalog
|
||||||
|
// entries replaced while jobs are out are freed once none are (background_retire). A newer search
|
||||||
|
// makes the older ones stop early, and their results are dropped.
|
||||||
|
|
||||||
|
BACKGROUND_SEARCH_CHUNK :: 256; // captures matched per job
|
||||||
|
|
||||||
|
Search_Item :: struct {
|
||||||
|
index: s32;
|
||||||
|
entry: *Catalog_Entry; // null while not loaded
|
||||||
|
name: string; // the capture's; never freed
|
||||||
|
}
|
||||||
|
|
||||||
|
background: struct {
|
||||||
|
group: Thread_Group;
|
||||||
|
started: bool;
|
||||||
|
jobs_out: s64; // UI thread: handed out and not yet taken back
|
||||||
|
search_generation: s64; // the newest search; workers read it to stop early (atomic)
|
||||||
|
searching: bool; // the newest search's results are not back yet
|
||||||
|
stats_generation: s64; // the newest Statistics job
|
||||||
|
stats_out: bool;
|
||||||
|
retired: [..] *Catalog_Entry; // replaced while jobs were out
|
||||||
|
};
|
||||||
|
|
||||||
|
background_startup :: () {
|
||||||
|
background.retired.allocator = context.default_allocator;
|
||||||
|
threads := clamp(get_number_of_processors(.ALL_LOGICAL) - 2, 2, 16);
|
||||||
|
// Statistics count words in a table in temporary storage: a few MB on the full library.
|
||||||
|
init(*background.group, cast(s32) threads, background_proc, temporary_storage_size = 4 * 1024 * 1024, group_name = "background");
|
||||||
|
background.group.logging = false;
|
||||||
|
start(*background.group);
|
||||||
|
background.started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On the shutdown thread: stops the workers. Jobs still out are left to the process's end.
|
||||||
|
background_shutdown :: () {
|
||||||
|
if background.started shutdown(*background.group);
|
||||||
|
background.started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A catalog entry the UI thread no longer points at: freed now, or once no job can be reading it.
|
||||||
|
background_retire :: (entry: *Catalog_Entry) {
|
||||||
|
if !entry return;
|
||||||
|
if background.jobs_out array_add(*background.retired, entry);
|
||||||
|
else catalog_entry_free(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The captures shown, as a snapshot a job can read. Heap.
|
||||||
|
background_snapshot_visible :: () -> [] Search_Item {
|
||||||
|
items := NewArray(viewer.visible.count, Search_Item,, context.default_allocator);
|
||||||
|
for viewer.visible {
|
||||||
|
capture := *viewer.captures[it];
|
||||||
|
items[it_index] = .{it, capture.catalog, capture.name};
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each frame, on the UI thread: takes back what the workers finished, frees retired entries once
|
||||||
|
// nothing is out, and starts a search when the shown captures need working out again.
|
||||||
|
background_frame :: () {
|
||||||
|
if !background.started return;
|
||||||
|
for work: get_completed_work(*background.group) {
|
||||||
|
job := cast(*Background_Job) work;
|
||||||
|
background.jobs_out -= 1;
|
||||||
|
if job.kind == {
|
||||||
|
case .SEARCH_CHUNK;
|
||||||
|
request := job.request;
|
||||||
|
request.chunk_matches[job.chunk] = job.matches;
|
||||||
|
request.chunks_back += 1;
|
||||||
|
if request.chunks_back == request.chunk_matches.count {
|
||||||
|
if request.generation == background.search_generation {
|
||||||
|
merge := New(Background_Job,, context.default_allocator);
|
||||||
|
merge.* = .{kind = .SEARCH_MERGE, generation = request.generation, request = request};
|
||||||
|
background_add(merge);
|
||||||
|
} else {
|
||||||
|
search_request_free(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .SEARCH_MERGE;
|
||||||
|
if job.generation == background.search_generation {
|
||||||
|
array_free(viewer.visible);
|
||||||
|
viewer.visible = job.visible;
|
||||||
|
viewer.visible_generation += 1;
|
||||||
|
background.searching = false;
|
||||||
|
} else {
|
||||||
|
array_free(job.visible);
|
||||||
|
}
|
||||||
|
search_request_free(job.request);
|
||||||
|
case .STATS;
|
||||||
|
stats_result_arrived(job.generation, job.stats);
|
||||||
|
background.stats_out = false;
|
||||||
|
array_free(job.items,, context.default_allocator);
|
||||||
|
free(job.model,, context.default_allocator);
|
||||||
|
}
|
||||||
|
free(job,, context.default_allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !background.jobs_out && background.retired.count {
|
||||||
|
for background.retired catalog_entry_free(it);
|
||||||
|
array_reset_keeping_memory(*background.retired);
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewer.visible_dirty background_search_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches the search against every capture not removed, in chunks on the workers; the results
|
||||||
|
// replace viewer.visible when they are all back.
|
||||||
|
background_search_start :: () {
|
||||||
|
// While the catalog loads under a search, matches arrive every frame: a few searches a second do.
|
||||||
|
if viewer.search_terms.count && !viewer.sorted_by_date && to_float64_seconds(current_time_monotonic() - viewer.visible_rebuilt_at) < 0.25 return;
|
||||||
|
viewer.visible_dirty = false;
|
||||||
|
viewer.visible_rebuilt_at = current_time_monotonic();
|
||||||
|
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
generation := background.search_generation + 1;
|
||||||
|
atomic_write(*background.search_generation, generation); // older chunks see it and stop
|
||||||
|
background.searching = true;
|
||||||
|
|
||||||
|
request := New(Search_Request);
|
||||||
|
request.generation = generation;
|
||||||
|
request.by_date = viewer.sorted_by_date;
|
||||||
|
request.terms = NewArray(viewer.search_terms.count, Search_Term);
|
||||||
|
for viewer.search_terms request.terms[it_index] = .{it.field, copy_string(it.text)};
|
||||||
|
items: [..] Search_Item;
|
||||||
|
array_reserve(*items, viewer_capture_count());
|
||||||
|
for * viewer.captures if !it.removed array_add(*items, .{cast(s32) it_index, it.catalog, it.name});
|
||||||
|
request.items = items;
|
||||||
|
chunks := max((items.count + BACKGROUND_SEARCH_CHUNK - 1) / BACKGROUND_SEARCH_CHUNK, 1);
|
||||||
|
request.chunk_matches = NewArray(chunks, [..] Search_Item);
|
||||||
|
|
||||||
|
for chunk: 0..chunks - 1 {
|
||||||
|
job := New(Background_Job);
|
||||||
|
first := chunk * BACKGROUND_SEARCH_CHUNK;
|
||||||
|
job.* = .{kind = .SEARCH_CHUNK, generation = generation, request = request, chunk = chunk,
|
||||||
|
first = first, count = clamp(items.count - first, 0, BACKGROUND_SEARCH_CHUNK)};
|
||||||
|
background_add(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Works out the Statistics window's numbers for items (taken over by the job) with model.
|
||||||
|
background_stats_start :: (items: [] Search_Item, model: string) {
|
||||||
|
background.stats_generation += 1;
|
||||||
|
background.stats_out = true;
|
||||||
|
job := New(Background_Job,, context.default_allocator);
|
||||||
|
job.* = .{kind = .STATS, generation = background.stats_generation, items = items, model = copy_string(model,, context.default_allocator)};
|
||||||
|
background_add(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
#scope_file
|
||||||
|
|
||||||
|
Background_Kind :: enum u8 { SEARCH_CHUNK; SEARCH_MERGE; STATS; }
|
||||||
|
|
||||||
|
Background_Job :: struct {
|
||||||
|
kind: Background_Kind;
|
||||||
|
generation: s64;
|
||||||
|
|
||||||
|
// SEARCH_CHUNK: request.items[first..first+count-1], matched into matches, in display order.
|
||||||
|
// SEARCH_MERGE: every chunk's matches, into visible.
|
||||||
|
request: *Search_Request;
|
||||||
|
chunk, first, count: s64;
|
||||||
|
matches: [..] Search_Item;
|
||||||
|
visible: [..] s32;
|
||||||
|
|
||||||
|
// STATS
|
||||||
|
items: [] Search_Item;
|
||||||
|
model: string;
|
||||||
|
stats: *Stats_Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Search_Request :: struct {
|
||||||
|
generation: s64;
|
||||||
|
terms: [] Search_Term;
|
||||||
|
items: [] Search_Item;
|
||||||
|
by_date: bool; // newest first; else in the captures' own order
|
||||||
|
chunk_matches: [] [..] Search_Item; // each chunk's, as they come back
|
||||||
|
chunks_back: s64;
|
||||||
|
}
|
||||||
|
|
||||||
|
background_add :: (job: *Background_Job) {
|
||||||
|
background.jobs_out += 1;
|
||||||
|
add_work(*background.group, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
search_request_free :: (request: *Search_Request) {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
for request.terms free(it.text);
|
||||||
|
array_free(request.terms);
|
||||||
|
array_free(request.items);
|
||||||
|
for request.chunk_matches array_free(it);
|
||||||
|
array_free(request.chunk_matches);
|
||||||
|
free(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On a worker.
|
||||||
|
background_proc :: (group: *Thread_Group, thread: *Thread, work: *void) -> Thread_Continue_Status {
|
||||||
|
context.allocator = context.default_allocator;
|
||||||
|
job := cast(*Background_Job) work;
|
||||||
|
if job.kind == {
|
||||||
|
case .SEARCH_CHUNK; search_chunk(job);
|
||||||
|
case .SEARCH_MERGE; search_merge(job);
|
||||||
|
case .STATS; job.stats = stats_compute(job.items, job.model);
|
||||||
|
}
|
||||||
|
reset_temporary_storage();
|
||||||
|
return .CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
search_chunk :: (job: *Background_Job) {
|
||||||
|
request := job.request;
|
||||||
|
for i: job.first..job.first + job.count - 1 {
|
||||||
|
if (i & 63) == 0 && atomic_read(*background.search_generation) != job.generation return; // a newer search: these are wasted
|
||||||
|
item := request.items[i];
|
||||||
|
if search_matches(item.entry, request.terms) array_add(*job.matches, item);
|
||||||
|
}
|
||||||
|
if request.by_date quick_sort(job.matches, search_order_by_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every chunk's matches as one list. In date order, each chunk is sorted already, so they are merged
|
||||||
|
// pairwise; in the captures' own order, chunks follow one another.
|
||||||
|
search_merge :: (job: *Background_Job) {
|
||||||
|
request := job.request;
|
||||||
|
runs: [..] [] Search_Item;
|
||||||
|
runs.allocator = temp;
|
||||||
|
for request.chunk_matches array_add(*runs, it);
|
||||||
|
if request.by_date {
|
||||||
|
while runs.count > 1 {
|
||||||
|
next: [..] [] Search_Item;
|
||||||
|
next.allocator = temp;
|
||||||
|
for i: 0..(runs.count + 1) / 2 - 1 {
|
||||||
|
if 2 * i + 1 < runs.count array_add(*next, merge_two(runs[2 * i], runs[2 * i + 1]));
|
||||||
|
else array_add(*next, runs[2 * i]);
|
||||||
|
}
|
||||||
|
runs = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total := 0;
|
||||||
|
for runs total += it.count;
|
||||||
|
array_reserve(*job.visible, total);
|
||||||
|
for run: runs for run array_add(*job.visible, it.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two lists in date order as one. Temp.
|
||||||
|
merge_two :: (a: [] Search_Item, b: [] Search_Item) -> [] Search_Item {
|
||||||
|
result := NewArray(a.count + b.count, Search_Item,, temp);
|
||||||
|
i, j := 0, 0;
|
||||||
|
for * result {
|
||||||
|
if j >= b.count || (i < a.count && search_order_by_date(a[i], b[j]) <= 0) { it.* = a[i]; i += 1; }
|
||||||
|
else { it.* = b[j]; j += 1; }
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
search_order_by_date :: (a: Search_Item, b: Search_Item) -> s64 {
|
||||||
|
return viewer_display_order(ifx a.entry then a.entry.date else "", a.name, ifx b.entry then b.entry.date else "", b.name);
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
// The Statistics window: what OCR found in the captures shown (the whole library, or a search's
|
// The Statistics window: what OCR found in the captures shown (the whole library, or a search's
|
||||||
// matches), by the model chosen in the OCR window. Worked out from the catalog, in memory, again when
|
// matches), by the model chosen in the OCR window. Worked out from the catalog, in memory, on a
|
||||||
// the catalog, the search or the model changes; at most once a second while a scan adds readings.
|
// background worker (app/background.jai), again when the catalog, the search or the model changes;
|
||||||
|
// at most once a second while a scan adds readings. The window shows the last numbers until then.
|
||||||
|
|
||||||
STATS_WINDOW :: "Statistics";
|
STATS_WINDOW :: "Statistics";
|
||||||
STATS_TOP_CAPTURES :: 10;
|
STATS_TOP_CAPTURES :: 10;
|
||||||
@ -15,32 +16,54 @@ Stats_Year :: struct { year: string; captures, with_text, words: s64; }
|
|||||||
Stats_Capture :: struct { index: s32; words: s64; }
|
Stats_Capture :: struct { index: s32; words: s64; }
|
||||||
Stats_Word :: struct { word: string; count: s64; }
|
Stats_Word :: struct { word: string; count: s64; }
|
||||||
|
|
||||||
stats_ui: struct {
|
// The numbers, worked out on a worker by stats_compute. Heap, with everything it holds.
|
||||||
window_open: bool;
|
Stats_Result :: struct {
|
||||||
|
model: string;
|
||||||
// What the numbers were worked out from; any change makes them stale.
|
|
||||||
model: string; // heap
|
|
||||||
catalog_generation, visible_generation: s64;
|
|
||||||
computed: bool;
|
|
||||||
computed_at: Apollo_Time;
|
|
||||||
compute_ms: float64;
|
compute_ms: float64;
|
||||||
|
|
||||||
captures, read, with_text: s64;
|
captures, read, with_text: s64;
|
||||||
lines, words, characters, cjk_characters, turned_lines, low_confidence_lines: s64;
|
lines, words, characters, cjk_characters, turned_lines, low_confidence_lines: s64;
|
||||||
confidence_sum: float64;
|
confidence_sum: float64;
|
||||||
words_median, words_p90, words_p99, words_max: s64;
|
words_median, words_p90, words_p99, words_max: s64;
|
||||||
buckets: [STATS_BUCKET_TOPS.count] s64;
|
buckets: [STATS_BUCKET_TOPS.count] s64;
|
||||||
years: [..] Stats_Year; // oldest first; year strings on the heap
|
years: [..] Stats_Year; // oldest first
|
||||||
most_words: [..] Stats_Capture; // most first
|
most_words: [..] Stats_Capture; // most first
|
||||||
common_words: [..] Stats_Word; // commonest first; words on the heap
|
common_words: [..] Stats_Word; // commonest first
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_ui: struct {
|
||||||
|
window_open: bool;
|
||||||
|
result: *Stats_Result; // the numbers shown; null until the first are back
|
||||||
|
|
||||||
|
// What the last job was asked for; any change makes the numbers stale.
|
||||||
|
model: string; // heap
|
||||||
|
catalog_generation, visible_generation: s64;
|
||||||
|
requested: bool;
|
||||||
|
requested_at: Apollo_Time;
|
||||||
};
|
};
|
||||||
|
|
||||||
stats_ui_shutdown :: () {
|
stats_ui_shutdown :: () {
|
||||||
stats_free_results();
|
stats_result_free(stats_ui.result);
|
||||||
|
stats_ui.result = null;
|
||||||
free(stats_ui.model,, context.default_allocator);
|
free(stats_ui.model,, context.default_allocator);
|
||||||
array_free(stats_ui.years);
|
}
|
||||||
array_free(stats_ui.most_words);
|
|
||||||
array_free(stats_ui.common_words);
|
// From background_frame: a job's numbers. The newest are shown; older ones are dropped.
|
||||||
|
stats_result_arrived :: (generation: s64, result: *Stats_Result) {
|
||||||
|
if generation != background.stats_generation { stats_result_free(result); return; }
|
||||||
|
stats_result_free(stats_ui.result);
|
||||||
|
stats_ui.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_result_free :: (s: *Stats_Result) {
|
||||||
|
if !s return;
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
free(s.model);
|
||||||
|
for s.years free(it.year);
|
||||||
|
for s.common_words free(it.word);
|
||||||
|
array_free(s.years);
|
||||||
|
array_free(s.most_words);
|
||||||
|
array_free(s.common_words);
|
||||||
|
free(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The model the numbers are for: the one chosen in the OCR window.
|
// The model the numbers are for: the one chosen in the OCR window.
|
||||||
@ -50,17 +73,31 @@ stats_model :: () -> string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats_ui_frame :: () { using ImGui;
|
stats_ui_frame :: () { using ImGui;
|
||||||
if app.stats_and_exit && viewer.captures.count && viewer.catalog_loaded >= viewer.captures.count && !ocr_ui.thread_busy {
|
// -stats: once the catalog and the search are settled, at once, on this thread.
|
||||||
stats_compute(stats_model());
|
if app.stats_and_exit && viewer.captures.count && viewer.catalog_loaded >= viewer.captures.count && !ocr_ui.thread_busy
|
||||||
|
&& !viewer.visible_dirty && !background.searching {
|
||||||
|
items := background_snapshot_visible();
|
||||||
|
stats_result_arrived(background.stats_generation, stats_compute(items, stats_model()));
|
||||||
|
array_free(items,, context.default_allocator);
|
||||||
stats_log();
|
stats_log();
|
||||||
app.stats_and_exit = false;
|
app.stats_and_exit = false;
|
||||||
app.should_exit = true;
|
app.should_exit = true;
|
||||||
}
|
}
|
||||||
if !stats_ui.window_open return;
|
if !stats_ui.window_open return;
|
||||||
|
|
||||||
|
// A new job when the numbers are stale and none is out: at most one a second.
|
||||||
model := stats_model();
|
model := stats_model();
|
||||||
stale := !stats_ui.computed || stats_ui.model != model
|
stale := !stats_ui.requested || stats_ui.model != model
|
||||||
|| stats_ui.catalog_generation != viewer.catalog_generation || stats_ui.visible_generation != viewer.visible_generation;
|
|| stats_ui.catalog_generation != viewer.catalog_generation || stats_ui.visible_generation != viewer.visible_generation;
|
||||||
if stale && (!stats_ui.computed || to_float64_seconds(current_time_monotonic() - stats_ui.computed_at) >= 1) stats_compute(model);
|
if stale && !background.stats_out && !background.searching
|
||||||
|
&& (!stats_ui.requested || to_float64_seconds(current_time_monotonic() - stats_ui.requested_at) >= 1) {
|
||||||
|
free(stats_ui.model,, context.default_allocator);
|
||||||
|
stats_ui.model = copy_string(model,, context.default_allocator);
|
||||||
|
stats_ui.catalog_generation, stats_ui.visible_generation = viewer.catalog_generation, viewer.visible_generation;
|
||||||
|
stats_ui.requested = true;
|
||||||
|
stats_ui.requested_at = current_time_monotonic();
|
||||||
|
background_stats_start(background_snapshot_visible(), model);
|
||||||
|
}
|
||||||
|
|
||||||
SetNextWindowSize(.{560, 640}, .FirstUseEver);
|
SetNextWindowSize(.{560, 640}, .FirstUseEver);
|
||||||
if Begin(STATS_WINDOW, *stats_ui.window_open) {
|
if Begin(STATS_WINDOW, *stats_ui.window_open) {
|
||||||
@ -73,7 +110,8 @@ stats_ui_frame :: () { using ImGui;
|
|||||||
|
|
||||||
// The numbers, as the window shows them, to the log: for -stats.
|
// The numbers, as the window shows them, to the log: for -stats.
|
||||||
stats_log :: () {
|
stats_log :: () {
|
||||||
s := *stats_ui;
|
s := stats_ui.result;
|
||||||
|
if !s return;
|
||||||
log("[stats] % with %: % captures, % read, % with text (%), % lines, % words, % characters (% CJK), % at an angle, mean confidence %, % under %, worked out in % ms.",
|
log("[stats] % with %: % captures, % read, % with text (%), % lines, % words, % characters (% CJK), % at an angle, mean confidence %, % under %, worked out in % ms.",
|
||||||
viewer_folder(), s.model, s.captures, s.read, s.with_text, stats_percent(s.with_text, s.read), s.lines, s.words, s.characters, s.cjk_characters,
|
viewer_folder(), s.model, s.captures, s.read, s.with_text, stats_percent(s.with_text, s.read), s.lines, s.words, s.characters, s.cjk_characters,
|
||||||
s.turned_lines, FF(s.confidence_sum / max(s.lines, 1), 3), s.low_confidence_lines, FF(STATS_LOW_CONFIDENCE, 1), FF(s.compute_ms, 1));
|
s.turned_lines, FF(s.confidence_sum / max(s.lines, 1), 3), s.low_confidence_lines, FF(STATS_LOW_CONFIDENCE, 1), FF(s.compute_ms, 1));
|
||||||
@ -86,7 +124,8 @@ stats_log :: () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats_window_contents :: () { using ImGui;
|
stats_window_contents :: () { using ImGui;
|
||||||
s := *stats_ui;
|
s := stats_ui.result;
|
||||||
|
if !s { TextColored(app_theme.palette.dim, "Working them out..."); return; }
|
||||||
shown := ifx viewer.search_terms.count then "the captures matching the search" else "the library";
|
shown := ifx viewer.search_terms.count then "the captures matching the search" else "the library";
|
||||||
TextWrapped("OCR with % over %: % captures.", s.model, shown, stats_number(s.captures));
|
TextWrapped("OCR with % over %: % captures.", s.model, shown, stats_number(s.captures));
|
||||||
if viewer.catalog_loaded < viewer.captures.count {
|
if viewer.catalog_loaded < viewer.captures.count {
|
||||||
@ -171,17 +210,17 @@ stats_window_contents :: () { using ImGui;
|
|||||||
if SmallButton(label) viewer_set_search(tprint("ocr:%", it.word));
|
if SmallButton(label) viewer_set_search(tprint("ocr:%", it.word));
|
||||||
}
|
}
|
||||||
Spacing();
|
Spacing();
|
||||||
TextColored(app_theme.palette.dim, "Worked out in % ms.", FF(s.compute_ms, 1));
|
TextColored(app_theme.palette.dim, "Worked out in % ms on a background thread%.", FF(s.compute_ms, 1), ifx background.stats_out then "; updating" else "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recounts everything over viewer.visible for model.
|
#scope_export
|
||||||
stats_compute :: (model: string) {
|
|
||||||
|
// Counts everything over items for model. On any thread: it reads only items and their catalog
|
||||||
|
// entries. The result, and all it holds, from the default allocator.
|
||||||
|
stats_compute :: (items: [] Search_Item, model: string) -> *Stats_Result {
|
||||||
start := current_time_monotonic();
|
start := current_time_monotonic();
|
||||||
s := *stats_ui;
|
s := New(Stats_Result,, context.default_allocator);
|
||||||
stats_free_results();
|
|
||||||
free(s.model,, context.default_allocator);
|
|
||||||
s.model = copy_string(model,, context.default_allocator);
|
s.model = copy_string(model,, context.default_allocator);
|
||||||
s.catalog_generation, s.visible_generation = viewer.catalog_generation, viewer.visible_generation;
|
|
||||||
s.years.allocator = context.default_allocator;
|
s.years.allocator = context.default_allocator;
|
||||||
s.most_words.allocator = context.default_allocator;
|
s.most_words.allocator = context.default_allocator;
|
||||||
s.common_words.allocator = context.default_allocator;
|
s.common_words.allocator = context.default_allocator;
|
||||||
@ -193,9 +232,10 @@ stats_compute :: (model: string) {
|
|||||||
words: Table(string, s64); // keys in temp
|
words: Table(string, s64); // keys in temp
|
||||||
words.allocator = temp;
|
words.allocator = temp;
|
||||||
|
|
||||||
for index: viewer.visible {
|
for item: items {
|
||||||
|
index := item.index;
|
||||||
s.captures += 1;
|
s.captures += 1;
|
||||||
entry := viewer.captures[index].catalog;
|
entry := item.entry;
|
||||||
year_text := ifx entry && entry.date.count >= 4 then slice(entry.date, 0, 4) else "undated";
|
year_text := ifx entry && entry.date.count >= 4 then slice(entry.date, 0, 4) else "undated";
|
||||||
year := find_or_add(*years, year_text);
|
year := find_or_add(*years, year_text);
|
||||||
year.year = year_text;
|
year.year = year_text;
|
||||||
@ -213,7 +253,7 @@ stats_compute :: (model: string) {
|
|||||||
x0, y0, x1, y1 := line.x, line.y, line.x + line.width, line.y + line.height;
|
x0, y0, x1, y1 := line.x, line.y, line.x + line.width, line.y + line.height;
|
||||||
box := s32.[x0, y0, x1, y0, x1, y1, x0, y1];
|
box := s32.[x0, y0, x1, y0, x1, y1, x0, y1];
|
||||||
for corners if it != box[it_index] { s.turned_lines += 1; break; }
|
for corners if it != box[it_index] { s.turned_lines += 1; break; }
|
||||||
capture_words += stats_count_line(line.text, *words);
|
capture_words += stats_count_line(s, line.text, *words);
|
||||||
}
|
}
|
||||||
s.words += capture_words;
|
s.words += capture_words;
|
||||||
if section.lines.count { s.with_text += 1; year.with_text += 1; }
|
if section.lines.count { s.with_text += 1; year.with_text += 1; }
|
||||||
@ -246,15 +286,15 @@ stats_compute :: (model: string) {
|
|||||||
array_add(*s.common_words, .{copy_string(it.word,, context.default_allocator), it.count});
|
array_add(*s.common_words, .{copy_string(it.word,, context.default_allocator), it.count});
|
||||||
}
|
}
|
||||||
|
|
||||||
s.computed = true;
|
s.compute_ms = to_float64_seconds(current_time_monotonic() - start) * 1000;
|
||||||
s.computed_at = current_time_monotonic();
|
return s;
|
||||||
s.compute_ms = to_float64_seconds(s.computed_at - start) * 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#scope_file
|
||||||
|
|
||||||
// Adds the line's characters to the counts, and its Latin words to words; returns its words (runs
|
// Adds the line's characters to the counts, and its Latin words to words; returns its words (runs
|
||||||
// between spaces, numbers and symbols included).
|
// between spaces, numbers and symbols included).
|
||||||
stats_count_line :: (text: string, words: *Table(string, s64)) -> s64 {
|
stats_count_line :: (s: *Stats_Result, text: string, words: *Table(string, s64)) -> s64 {
|
||||||
s := *stats_ui;
|
|
||||||
count: s64;
|
count: s64;
|
||||||
in_word := false;
|
in_word := false;
|
||||||
latin_end := 0; // the end of the last Latin word, which its hyphens and apostrophes belong to
|
latin_end := 0; // the end of the last Latin word, which its hyphens and apostrophes belong to
|
||||||
@ -301,20 +341,6 @@ STATS_STOP_WORDS :: string.[
|
|||||||
"got", "off", "per", "via", "yes", "new",
|
"got", "off", "per", "via", "yes", "new",
|
||||||
];
|
];
|
||||||
|
|
||||||
stats_free_results :: () {
|
|
||||||
s := *stats_ui;
|
|
||||||
for s.years free(it.year,, context.default_allocator);
|
|
||||||
for s.common_words free(it.word,, context.default_allocator);
|
|
||||||
array_reset_keeping_memory(*s.years);
|
|
||||||
array_reset_keeping_memory(*s.most_words);
|
|
||||||
array_reset_keeping_memory(*s.common_words);
|
|
||||||
s.captures, s.read, s.with_text = 0, 0, 0;
|
|
||||||
s.lines, s.words, s.characters, s.cjk_characters, s.turned_lines, s.low_confidence_lines = 0, 0, 0, 0, 0, 0;
|
|
||||||
s.confidence_sum = 0;
|
|
||||||
s.words_median, s.words_p90, s.words_p99, s.words_max = 0, 0, 0, 0;
|
|
||||||
for * s.buckets it.* = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 874477 as "874,477". Temp.
|
// 874477 as "874,477". Temp.
|
||||||
stats_number :: (n: s64) -> string {
|
stats_number :: (n: s64) -> string {
|
||||||
digits := tprint("%", abs(n));
|
digits := tprint("%", abs(n));
|
||||||
|
|||||||
@ -5,7 +5,8 @@
|
|||||||
//
|
//
|
||||||
// When the workers have no previews to decode they load the catalog (app/catalog.jai), what search
|
// When the workers have no previews to decode they load the catalog (app/catalog.jai), what search
|
||||||
// and sorting need from each capture. The grid shows viewer.visible: the captures in display order,
|
// and sorting need from each capture. The grid shows viewer.visible: the captures in display order,
|
||||||
// newest first once the catalog is complete, and only those matching the search.
|
// newest first once the catalog is complete, and only those matching the search. Searching runs on
|
||||||
|
// background workers (app/background.jai), so the UI thread never waits on it.
|
||||||
//
|
//
|
||||||
// A capture keeps its index for as long as the app runs: the workers, the OCR scan and Stats refer
|
// A capture keeps its index for as long as the app runs: the workers, the OCR scan and Stats refer
|
||||||
// to captures by index. Refresh (viewer_refresh) marks the files gone from the folder as removed and
|
// to captures by index. Refresh (viewer_refresh) marks the files gone from the folder as removed and
|
||||||
@ -165,6 +166,7 @@ viewer_startup :: () {
|
|||||||
thread_start(it);
|
thread_start(it);
|
||||||
viewer.workers_started += 1;
|
viewer.workers_started += 1;
|
||||||
}
|
}
|
||||||
|
background_startup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// On the shutdown thread: stops the workers. The textures go with the device.
|
// On the shutdown thread: stops the workers. The textures go with the device.
|
||||||
@ -174,6 +176,7 @@ viewer_shutdown :: () {
|
|||||||
unlock(*viewer.mutex);
|
unlock(*viewer.mutex);
|
||||||
for 1..viewer.workers_started signal(*viewer.wake);
|
for 1..viewer.workers_started signal(*viewer.wake);
|
||||||
for 0..viewer.workers_started - 1 thread_is_done(*viewer.workers[it], -1);
|
for 0..viewer.workers_started - 1 thread_is_done(*viewer.workers[it], -1);
|
||||||
|
background_shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- the workers
|
// ---------------------------------------------------------------- the workers
|
||||||
@ -377,7 +380,7 @@ viewer_frame_gpu_work :: () {
|
|||||||
|
|
||||||
viewer_evict();
|
viewer_evict();
|
||||||
viewer_apply_catalog_updates();
|
viewer_apply_catalog_updates();
|
||||||
if viewer.visible_dirty viewer_rebuild_visible();
|
background_frame(); // takes back search results, and starts a search when visible_dirty (app/background.jai)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a preview was last in view or wanted ahead of it. A prefetched preview has not been drawn
|
// When a preview was last in view or wanted ahead of it. A prefetched preview has not been drawn
|
||||||
@ -399,7 +402,7 @@ viewer_apply_catalog_updates :: () {
|
|||||||
capture := *viewer.captures[it.index];
|
capture := *viewer.captures[it.index];
|
||||||
if !capture.catalog viewer.catalog_loaded += 1;
|
if !capture.catalog viewer.catalog_loaded += 1;
|
||||||
if it.entry || !capture.catalog {
|
if it.entry || !capture.catalog {
|
||||||
catalog_entry_free(capture.catalog);
|
background_retire(capture.catalog); // a search or Stats job may be reading it
|
||||||
capture.catalog = it.entry;
|
capture.catalog = it.entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -472,6 +475,9 @@ viewer_mark_removed :: (index: s32) {
|
|||||||
if found array_ordered_remove_by_index(*viewer.with_texture, position);
|
if found array_ordered_remove_by_index(*viewer.with_texture, position);
|
||||||
}
|
}
|
||||||
if viewer.full_index == index viewer.full_open = false;
|
if viewer.full_index == index viewer.full_open = false;
|
||||||
|
// Out of the grid at once; the search that visible_dirty starts would take it out too, a frame or two later.
|
||||||
|
found, position := array_find(viewer.visible, index);
|
||||||
|
if found array_ordered_remove_by_index(*viewer.visible, position);
|
||||||
viewer.catalog_generation += 1;
|
viewer.catalog_generation += 1;
|
||||||
viewer.visible_dirty = true;
|
viewer.visible_dirty = true;
|
||||||
viewer.visible_rebuilt_at = .{};
|
viewer.visible_rebuilt_at = .{};
|
||||||
@ -629,26 +635,6 @@ viewer_search_changed :: () {
|
|||||||
viewer.visible_rebuilt_at = .{}; // at once, not throttled
|
viewer.visible_rebuilt_at = .{}; // at once, not throttled
|
||||||
}
|
}
|
||||||
|
|
||||||
viewer_rebuild_visible :: () {
|
|
||||||
// While the catalog loads under a search, matches arrive every frame: a few rebuilds a second do.
|
|
||||||
if viewer.search_terms.count && !viewer.sorted_by_date && to_float64_seconds(current_time_monotonic() - viewer.visible_rebuilt_at) < 0.25 return;
|
|
||||||
viewer.visible_dirty = false;
|
|
||||||
viewer.visible_rebuilt_at = current_time_monotonic();
|
|
||||||
viewer.visible_generation += 1;
|
|
||||||
|
|
||||||
array_reset_keeping_memory(*viewer.visible);
|
|
||||||
for viewer.captures {
|
|
||||||
if it.removed continue;
|
|
||||||
if viewer.search_terms.count && !search_matches(it.catalog, viewer.search_terms) continue;
|
|
||||||
array_add(*viewer.visible, cast(s32) it_index);
|
|
||||||
}
|
|
||||||
if viewer.sorted_by_date {
|
|
||||||
quick_sort(viewer.visible, (a: s32, b: s32) -> s64 {
|
|
||||||
ca, cb := *viewer.captures[a], *viewer.captures[b];
|
|
||||||
return viewer_display_order(ifx ca.catalog then ca.catalog.date else "", ca.name, ifx cb.catalog then cb.catalog.date else "", cb.name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The grid's order: newest CaptureDate first, undated captures last, ties by name, newest first.
|
// The grid's order: newest CaptureDate first, undated captures last, ties by name, newest first.
|
||||||
viewer_display_order :: (date_a: string, name_a: string, date_b: string, name_b: string) -> s64 {
|
viewer_display_order :: (date_a: string, name_a: string, date_b: string, name_b: string) -> s64 {
|
||||||
@ -766,6 +752,7 @@ viewer_ui :: () { using ImGui;
|
|||||||
SameLine();
|
SameLine();
|
||||||
if viewer.search_terms.count Text("% of % captures", viewer.visible.count, viewer_capture_count());
|
if viewer.search_terms.count Text("% of % captures", viewer.visible.count, viewer_capture_count());
|
||||||
else Text("% captures", viewer_capture_count());
|
else Text("% captures", viewer_capture_count());
|
||||||
|
if background.searching { SameLine(); TextColored(app_theme.palette.dim, "searching..."); }
|
||||||
if viewer.catalog_loaded < viewer.captures.count {
|
if viewer.catalog_loaded < viewer.captures.count {
|
||||||
SameLine();
|
SameLine();
|
||||||
TextColored(app_theme.palette.dim, "(loading the catalog: % of %; sorted by name until then)", viewer.catalog_loaded, viewer.captures.count);
|
TextColored(app_theme.palette.dim, "(loading the catalog: % of %; sorted by name until then)", viewer.catalog_loaded, viewer.captures.count);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user