[0.1.15] Startup: a catalog cache, the catalog on the background threads, rpmalloc, and [startup] timings
The catalog is kept in .catalog-cache in the library, each entry with the size and modified time of the file it was read from; a start takes the entries whose file is unchanged from it and reads only the rest. It is saved once the catalog is complete and has changed (at most every 30 s) and at shutdown. After a reboot the catalog was complete 145 ms after launch, against 3.5 s reading every file. The catalog loads on the background Thread_Group, every worker of it, instead of the four preview workers. rpmalloc (modules/Default_Allocator, 2.0.1-jai.16) takes the place of Jai's single locked heap: entries from the cache 380 -> 50 ms, and reading the files now scales with threads. thread_idle is called where workers idle. check_targets.jai checks other OSes with Jai's own allocator, as rpmalloc runs at compile time with the target's OS layer. -cold drops the library from the system's file cache first; -no-catalog-cache and -catalog-threads N are for measuring. [startup] log lines time the listing, the first frame, the cache, the first preview and the complete catalog.
This commit is contained in:
parent
9ac9a0a4e9
commit
22cad98500
@ -350,6 +350,9 @@ then in that order instead of by date.
|
|||||||
| `-library FOLDER` | Shows the `.mcap` files in FOLDER (and its folders) instead of `D:\McapIndex\mcap`. |
|
| `-library FOLDER` | Shows the `.mcap` files in FOLDER (and its folders) instead of `D:\McapIndex\mcap`. |
|
||||||
| `-import SOURCE DEST` | Imports the folder SOURCE into the library folder DEST, as the Import window does, then exits: 0, or 1 if any file failed. |
|
| `-import SOURCE DEST` | Imports the folder SOURCE into the library folder DEST, as the Import window does, then exits: 0, or 1 if any file failed. |
|
||||||
| `-search QUERY` | Starts with QUERY in the search box. |
|
| `-search QUERY` | Starts with QUERY in the search box. |
|
||||||
|
| `-cold` | Loads the library as on a cold start: its files and the catalog cache are dropped from the system's file cache first (Windows and Linux; not macOS, which allows it only with `sudo purge`). The `[startup]` lines in the log time each step. With `-stats`, a startup benchmark: `bin\mcap.exe -cold -stats`. |
|
||||||
|
| `-no-catalog-cache` | Reads every capture from its file, as without the catalog cache (for measuring); the cache is still written. |
|
||||||
|
| `-catalog-threads N` | Loads the catalog on N threads instead of all the background group's (for measuring). |
|
||||||
| `-scan` | Scans the library (the chosen OCR model, and labels when the label model is up) once it has loaded, then exits: 0, or 1 if any capture failed. |
|
| `-scan` | Scans the library (the chosen OCR model, and labels when the label model is up) once it has loaded, then exits: 0, or 1 if any capture failed. |
|
||||||
| `-stats` | Logs the Stats window's numbers once the catalog has loaded, then exits; with `-search`, for the matches, and the first ten are logged. |
|
| `-stats` | Logs the Stats window's numbers once the catalog has loaded, then exits; with `-search`, for the matches, and the first ten are logged. |
|
||||||
| `-record SECONDS` | Records the screen under the cursor that long (no overlay), puts it in the library, then exits: 0, or 1 if it failed. `-record-region X Y W H` records that region instead. `-record-quit` quits while still recording, to check the recording is kept. `-record-ui-visible` leaves the border and toolbar in screenshots, to look at them. |
|
| `-record SECONDS` | Records the screen under the cursor that long (no overlay), puts it in the library, then exits: 0, or 1 if it failed. `-record-region X Y W H` records that region instead. `-record-quit` quits while still recording, to check the recording is kept. `-record-ui-visible` leaves the border and toolbar in screenshots, to look at them. |
|
||||||
@ -373,6 +376,7 @@ scripts and for trying things on a copy of the library: `bin\mcap.exe -library D
|
|||||||
| Settings (theme, hotkeys, OCR model) | `%APPDATA%\mcap\settings.config` |
|
| Settings (theme, hotkeys, OCR model) | `%APPDATA%\mcap\settings.config` |
|
||||||
| Logs | `logs\<version>\` beside the program |
|
| Logs | `logs\<version>\` beside the program |
|
||||||
| The hash of every capture's image, for finding duplicates | `.import-index` in the library folder |
|
| The hash of every capture's image, for finding duplicates | `.import-index` in the library folder |
|
||||||
|
| The catalog cache: every capture's catalog entry, so a start reads one file instead of every capture (an entry is used while its file's size and modified time are unchanged; deleting it is safe) | `.catalog-cache` in the library folder |
|
||||||
| The folder each library folder was imported from | `.import-sources` in the library folder |
|
| The folder each library folder was imported from | `.import-sources` in the library folder |
|
||||||
| Which library folders are segmented | `.library-folders` in the library folder |
|
| Which library folders are segmented | `.library-folders` in the library folder |
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,24 @@
|
|||||||
options.output_executable_name = "check";
|
options.output_executable_name = "check";
|
||||||
options.output_path = ".build";
|
options.output_path = ".build";
|
||||||
|
|
||||||
|
// rpmalloc (modules/Default_Allocator) also runs at compile time, with the target's OS layer,
|
||||||
|
// which crashes on another OS. A check for another OS uses Jai's own allocator instead, through a
|
||||||
|
// module of the same name ahead of modules/ (with the thread_idle the app calls, doing nothing).
|
||||||
|
if target != OS {
|
||||||
|
jai_modules := trim_right(options.import_path[options.import_path.count - 1], "/");
|
||||||
|
shim := tprint("%/.build/check_modules", get_working_directory());
|
||||||
|
make_directory_if_it_does_not_exist(tprint("%/Default_Allocator", shim), recursive = true);
|
||||||
|
write_entire_file(tprint("%/Default_Allocator/module.jai", shim), tprint(#string SHIM
|
||||||
|
// Written by check_targets.jai: Jai's own allocator, for type-checking another OS.
|
||||||
|
#load "%/Default_Allocator/module.jai";
|
||||||
|
thread_idle :: () {}
|
||||||
|
SHIM, jai_modules));
|
||||||
|
paths: [..] string;
|
||||||
|
array_add(*paths, shim);
|
||||||
|
array_add(*paths, ..options.import_path);
|
||||||
|
options.import_path = paths;
|
||||||
|
}
|
||||||
|
|
||||||
w := compiler_create_workspace("check");
|
w := compiler_create_workspace("check");
|
||||||
set_build_options(options, w);
|
set_build_options(options, w);
|
||||||
compiler_begin_intercept(w);
|
compiler_begin_intercept(w);
|
||||||
@ -29,7 +47,7 @@ APP_NAME :: "mcap";
|
|||||||
APP_FOLDER_NAME :: "mcap";
|
APP_FOLDER_NAME :: "mcap";
|
||||||
GIT_BRANCH :: "check";
|
GIT_BRANCH :: "check";
|
||||||
GIT_HASH :: "-";
|
GIT_HASH :: "-";
|
||||||
VERSION :: "0.1.14";
|
VERSION :: "0.1.15";
|
||||||
BUILD_DATE :: "-";
|
BUILD_DATE :: "-";
|
||||||
DEBUG :: true;
|
DEBUG :: true;
|
||||||
COMPILE_TESTS :: true;
|
COMPILE_TESTS :: true;
|
||||||
@ -49,3 +67,5 @@ S, w);
|
|||||||
|
|
||||||
#import "Basic";
|
#import "Basic";
|
||||||
#import "Compiler";
|
#import "Compiler";
|
||||||
|
#import "File";
|
||||||
|
#import "String";
|
||||||
|
|||||||
@ -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.1.14";
|
VERSION :: "0.1.15";
|
||||||
// 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";
|
||||||
|
|||||||
148
modules/Default_Allocator/DIFFERENCES.md
Normal file
148
modules/Default_Allocator/DIFFERENCES.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# How this Default_Allocator differs from Jai's
|
||||||
|
|
||||||
|
This module replaces Jai's `modules/Default_Allocator` with the rpmalloc 2.0.1 port from `my_modules/rpmalloc` (installed with its `install.jai`). Jai finds the default allocator by module name, so the replacement is the directory that comes first on the import path. This document lists everything that behaves differently. Facts about Jai's module refer to Jai beta 0.2.030.
|
||||||
|
|
||||||
|
Everything here is tested by the port's suite, `my_modules/rpmalloc/tests`:
|
||||||
|
- `jai build.jai - swap` installs this module with `install.jai`, builds a program whose only allocator is the installed copy, runs that program, and type-checks it for macOS and Linux.
|
||||||
|
- The port's own suite (89 tests) covers the allocator code itself.
|
||||||
|
|
||||||
|
## 1. Which allocator you get
|
||||||
|
|
||||||
|
| Platform | Jai's Default_Allocator | This module |
|
||||||
|
|---|---|---|
|
||||||
|
| Windows x64 | Stripped-down rpmalloc 1.4.4: one heap shared by every thread behind one lock, and no thread caches | rpmalloc 2.0.1 (port), one heap per thread (**tested**) |
|
||||||
|
| Linux x64 | The same as on Windows x64 | The same as on Windows (**type-checked only**; there is no Linux machine) |
|
||||||
|
| macOS ARM64 | A thin wrapper over the C runtime's `malloc` / `realloc` / `free` | rpmalloc 2.0.1 (port) (**tested**) |
|
||||||
|
| macOS x64, Linux ARM64 | Linux ARM64: C runtime wrapper; macOS x64: stripped-down rpmalloc 1.4.4 | Builds, but untested |
|
||||||
|
| PS5, Switch 2, iOS, Android, WASM, 32-bit | Their own branches (rpmalloc 1.4.4 on PS5 and Switch 2; the C runtime wrapper elsewhere) | **Not supported.** The module stops with an `#assert`. Take `my_modules` off the import path for these targets. |
|
||||||
|
|
||||||
|
The CAPS string tells you which allocator is in use: `"rpmalloc 2.0.1 (Jai port) as Default_Allocator"`. Jai's reports `"stripped-down rpmalloc 1.4.4 intended as Default_Allocator"`, or `"Jai wrapper for C’s malloc/free/realloc functions."`.
|
||||||
|
|
||||||
|
## 2. Exported interface
|
||||||
|
|
||||||
|
| Name | Jai's | This module |
|
||||||
|
|---|---|---|
|
||||||
|
| `allocator` | Yes (rpmalloc branch only) | Yes |
|
||||||
|
| `allocator_proc` | Yes | Yes, same signature |
|
||||||
|
| `unsafe_reset()` | rpmalloc branch: forgets all state and starts over. C runtime branch: does nothing. | Releases every lock and keeps all state (§6) |
|
||||||
|
| `initialize_if_needed() -> bool` | Yes | Yes |
|
||||||
|
| `is_initialized() -> bool` | Yes | Yes |
|
||||||
|
| `finalize()` | Yes | Yes (the port's `finalize`) |
|
||||||
|
| `usable_size(ptr) -> s64` | Yes | Yes |
|
||||||
|
| `is_this_yours(heap, ptr) -> bool` | Walks the heap's span lists. It misses older large blocks (finding F4/M2, test HEAP-05) and the blocks of exited threads (HEAP-10). With a null heap it looks only in the one default heap. | Looks the address up in a bitmap of live spans and never reads memory it does not own. With a null heap it checks every thread heap (not first-class heaps). |
|
||||||
|
| `heap_free_all(heap)` | Yes | Yes |
|
||||||
|
| `global_statistics()` | A placeholder struct of 7 fields, always zero | rpmalloc's `Global_Statistics` (9 fields). It is zero unless `ENABLE_STATISTICS`. The memory committed now is `active`; `committed` and `decommitted` are running totals that only grow, as upstream. The pointer form `global_statistics(*stats)` also exists. |
|
||||||
|
| `heap_statistics(heap)` | `{sizecache, spancache}` | `{allocated_size, committed_size, mapped_size}`, zero unless `HEAP_STATISTICS` |
|
||||||
|
| `allocator_default_heap` | A global `*Heap` | **Not provided.** There is no single default heap; each thread has its own. |
|
||||||
|
| The rest of the rpmalloc API | No | `initialize`, `thread_collect`, `trim`, `rpmalloc`, `rpaligned_alloc`, `heap_acquire`, and the rest (see `my_modules/rpmalloc/module.jai`) |
|
||||||
|
|
||||||
|
**Module parameters.** `ENABLE_ASSERTS` and `ENABLE_VALIDATE_ARGS` have the same names and defaults (both `true`). The port's other parameters are added: statistics, decommit, unmap, first-class heaps, the huge cache and test hooks. `Runtime_Support` imports the module without parameters, so the context allocator always uses the defaults. Importing it with other parameters creates a separate instance with its own state; the context allocator is not affected.
|
||||||
|
|
||||||
|
## 3. Allocator protocol
|
||||||
|
|
||||||
|
| Mode | Jai's | This module |
|
||||||
|
|---|---|---|
|
||||||
|
| ALLOCATE / RESIZE / FREE with `data == null` | rpmalloc branch: the one default heap, behind a global lock. C runtime branch: `malloc`. | The calling thread's own heap, with no lock on the fast path. A block freed by another thread is handed back to its owning heap without blocking. |
|
||||||
|
| Out of memory | **Asserts** with asserts on (N1, tests SIZE-01 and SIZE-07) | Returns null |
|
||||||
|
| Negative or impossibly large size | Not caught (F11, SIZE-02 and SIZE-03). The C runtime wrapper panics in debug builds (N5). | Returns null; a refused RESIZE leaves the block intact |
|
||||||
|
| RESIZE `old_size` | Ignored | When the block moves, `old_size` bytes are copied; 0 means the whole old block. **Passing a positive `old_size` smaller than the live data loses the rest.** |
|
||||||
|
| RESIZE to the same size | Can move the block (I1, RE-02) | Keeps it |
|
||||||
|
| A refused RESIZE | Returns the old pointer (L6, RE-04) | Returns null |
|
||||||
|
| STARTUP | Does nothing; returns null | `initialize()`; returns null. `Process/posix.jai` stores this return value in `context.allocator.data`, where null means "the thread's heap". |
|
||||||
|
| SHUTDOWN | Forgets the default heap; later use fails (F3, LIFE-01) | `finalize()`; the next allocation initialises again |
|
||||||
|
| THREAD_START / THREAD_STOP | Do nothing | Set up / release the thread's heap. Both are optional: a thread gets its heap on its first allocation, and the heap is released when the thread exits. Jai's `Thread` module sends these only on Windows (N4). |
|
||||||
|
| CREATE_HEAP | rpmalloc branch: yes. C runtime branch: **asserts**. | Yes (with `FIRST_CLASS_HEAPS`, the default) |
|
||||||
|
| DESTROY_HEAP | Releases the heap; a new heap can then claim the destroyed heap's blocks (F5, HEAP-01). Blocks still in it leak: 1,970 MiB over 190 heaps (HEAP-11). | Frees every block in the heap, then releases it |
|
||||||
|
| IS_THIS_YOURS | rpmalloc branch: see §2. C runtime branch: **asserts**. | See §2 |
|
||||||
|
| CAPS flags | rpmalloc branch: general heap, threads, CREATE_HEAP, FREE, ACTUALLY_RESIZE, IS_THIS_YOURS. C runtime branch: no heaps, no IS_THIS_YOURS. | The same as the rpmalloc branch, on every platform |
|
||||||
|
|
||||||
|
**First-class heaps.** A heap from CREATE_HEAP is for one thread at a time, as in upstream rpmalloc. Putting it in a Context that several threads copy lets them race on it.
|
||||||
|
|
||||||
|
## 4. Threads
|
||||||
|
|
||||||
|
- **Heap per thread.** Each thread allocates from its own heap, kept in OS thread-local storage (`TlsAlloc`/`FlsAlloc`, or a pthread key). A Context copied to another thread does not share the heap.
|
||||||
|
- **Heaps of exited threads are reused.** A thread's heap is released when the thread exits, even without THREAD_STOP, and the next new thread takes it over. With Jai's module on Windows and Linux, all threads share one heap behind one lock: there is no per-thread state, but every allocation contends for that lock.
|
||||||
|
- **Freeing another thread's block.** A block freed by a thread other than its owner is queued without a lock. Its owner reuses it on that owner's next allocation, or on `thread_collect()`. If the owner has exited, or is idle in `thread_idle()`, the block is freed at once instead.
|
||||||
|
- **Thread pools: call `thread_idle()`.** A pool thread that sleeps makes no allocator calls, so blocks other threads free into its heap, and the heap's free pages, would stay committed until it wakes. Calling `Default_Allocator.thread_idle()` where a pool thread waits for work gives that memory back and parks the heap until the thread's next allocation. Jai's module has no per-thread state, so it needs no such call.
|
||||||
|
- **Partly used pages per thread.** Each heap keeps its own partly used pages, so a program with many threads holds some more memory in partly filled small pages (64 KiB each) than one shared heap would. Larger pages are committed as they fill.
|
||||||
|
|
||||||
|
## 5. Memory use
|
||||||
|
|
||||||
|
- **Retention.** The port keeps freed pages per thread and huge blocks in a cache of up to 128 MiB committed (`HUGE_CACHE_LIMIT_MB`; a program that frees and allocates many huge blocks may run faster with more, holding up to that much while it does). Cached entries idle for a second are decommitted; blocks beyond the budget are unmapped. It decommits free pages only after a delay. Jai's rpmalloc branch returns memory eagerly, and pays page faults for it on reuse (F1, RET-01: about 112 faults per warm cycle). `trim()` returns every free page of the calling thread's heap, plus the whole huge cache, at once. `thread_collect()` does the same for pages past their delay.
|
||||||
|
- **Address space.** Memory is reserved in 256 MiB spans and committed page by page. Virtual size therefore looks large; the committed size is what costs memory. A 128 KiB bitmap for IS_THIS_YOURS is mapped once.
|
||||||
|
- **Measured footprint** (MiB with the benchmark's live set allocated; `my_modules/rpmalloc/tests/results/BENCH-*.md`):
|
||||||
|
|
||||||
|
| Benchmark | Windows: Jai's | Windows: this | macOS: Jai's (C runtime) | macOS: this |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| churn_medium | 204 | 170 | 148 | 66 |
|
||||||
|
| churn_large | 157 | 260 | 128 | 7.8 |
|
||||||
|
| churn_huge | 296 | 452 | 28.5 | 4.2 |
|
||||||
|
|
||||||
|
- **macOS.** The port decommits with `MADV_FREE_REUSABLE`, so freed memory leaves the process's footprint.
|
||||||
|
|
||||||
|
## 6. fork()
|
||||||
|
|
||||||
|
`Process/posix.jai` calls `unsafe_reset()` in the child, then `allocator_proc(.STARTUP)`. Jai's rpmalloc branch forgets its whole state at that point, so every block allocated before the fork leaks in the child. This module instead releases each lock that another thread of the parent might have held: the global heap lock, the heap-creation flag, the huge-cache lock, and an initialisation another thread had started. Everything else stays as it was. The forking thread's heap and its blocks remain valid in the child. Like Jai's, it cannot repair a list another thread was in the middle of changing; that window is a few instructions under one of those locks. `jai build.jai - swap` checks this: without `unsafe_reset` the smoke test hangs, and with it the test passes.
|
||||||
|
|
||||||
|
## 7. Compile time
|
||||||
|
|
||||||
|
Code in `#run` allocates through this module as well; the swap smoke test checks this. Jai's module also works at compile time.
|
||||||
|
|
||||||
|
## 8. Build
|
||||||
|
|
||||||
|
- This module imports `Basic`, `Bit_Operations` and `Atomics`; Jai's imports nothing.
|
||||||
|
- Its code is about 4,000 lines; Jai's rpmalloc branch is about 2,000.
|
||||||
|
- Unsupported platforms fail to compile (§1).
|
||||||
|
- **One Context field.** The module adds `rpmalloc_default_allocator_thread_heap` (a pointer) to every program's `Context`, a cache of the thread's heap, so allocations and frees need no OS thread-local storage call. A second import of the module with other parameters must pass `CONTEXT_HEAP_CACHE = false`, since two imports cannot add the same field. Jai's module adds nothing to the Context.
|
||||||
|
- It works in programs built with `arithmetic_overflow_check = .FATAL`: no code relies on unsigned wraparound. In `my_modules/rpmalloc/tests`, `jai build.jai - swap overflow=fatal` and the port's suite with `overflow=fatal` test this. Both pass on Windows and macOS; Linux is type-checked only.
|
||||||
|
|
||||||
|
## 9. Speed
|
||||||
|
|
||||||
|
Median ns per operation, module defaults, from `my_modules/rpmalloc/tests/results/BENCH-*.md`; lower is better.
|
||||||
|
|
||||||
|
| Benchmark | Windows: Jai's | Windows: this | macOS: Jai's (C runtime) | macOS: this |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| pair_64 (alloc + free of 64 B) | 34.3 | 13.1 | 9.7 | 12.2 |
|
||||||
|
| pair_mixed (16..1024 B) | 256 | 15.5 | 23.6 | 13.4 |
|
||||||
|
| churn_medium (1..64 KiB) | 1059 | 51.7 | 74.0 | 53.2 |
|
||||||
|
| churn_large (64 KiB..1 MiB) | 4600 | 32.3 | 122 | 30.9 |
|
||||||
|
| **churn_huge (2..32 MiB)** | 9526 | **20245** | 618 | **4751** |
|
||||||
|
| realloc_double (64 B to 8 MiB) | 184662 | 15492 | 6373 | 6601 |
|
||||||
|
| mt_churn_8 (8 threads) | 515 | 2.3 | 18.0 | 1.9 |
|
||||||
|
| mt_producer (cross-thread frees) | 484 | 46.4 | 122 | 87.3 |
|
||||||
|
|
||||||
|
On Windows this module is faster everywhere except **churn_huge** (replacing blocks of 2 MiB and up), where it is 2× slower. On macOS, where Jai's module is the system `malloc`, it is level or faster except on pair_64 (0.8×) and churn_huge (7× slower). That gap is shelved in the plan.
|
||||||
|
|
||||||
|
## 10. Test results
|
||||||
|
|
||||||
|
The test suite was run against Jai's module and against the port:
|
||||||
|
|
||||||
|
| Platform | Jai's Default_Allocator | This module's code (the port) |
|
||||||
|
|---|---|---|
|
||||||
|
| Windows x64 (Jai's: rpmalloc 1.4.4) | 30 pass, 11 fail, 4 crash (41 skipped: capabilities it lacks) | 82 pass; every other test is SKIP or N/A |
|
||||||
|
| macOS ARM64 (Jai's: C runtime `malloc`) | 30 pass, 6 fail, 2 crash (48 skipped) | 83 pass; every other test is SKIP or N/A |
|
||||||
|
|
||||||
|
What Jai's module fails, by kind:
|
||||||
|
|
||||||
|
- **Correctness, Windows:**
|
||||||
|
- SIZE-01 and SIZE-07 assert when the OS refuses memory.
|
||||||
|
- SIZE-02 and SIZE-03 accept negative sizes.
|
||||||
|
- RE-02: a same-size resize moves the block.
|
||||||
|
- RE-04: a refused resize returns the old pointer.
|
||||||
|
- HEAP-01: a new heap claims a destroyed heap's blocks.
|
||||||
|
- HEAP-05 and HEAP-10: IS_THIS_YOURS misses blocks it owns.
|
||||||
|
- HEAP-11: destroyed heaps leak their blocks.
|
||||||
|
- LIFE-01: the allocator is unusable after SHUTDOWN.
|
||||||
|
- **Correctness, macOS:** SIZE-02 crashes on a negative size, and RE-04 is the same as on Windows.
|
||||||
|
- **Memory behaviour, Windows:** RET-01, RET-06, RET-10 and RET-14 fail on page faults. Freed memory goes back to the OS and faults in again on every reuse (up to 2,053 faults per 8 MiB cycle).
|
||||||
|
- **Memory behaviour, macOS:** OS-07, RET-02, RET-07, RET-11 and RET-12 fail because the system `malloc` keeps freed memory private (258 of 258 MiB after freeing everything). THR-09: blocks freed by another thread are not reused first.
|
||||||
|
|
||||||
|
One upstream bug remains open in the port: U15 (POSIX only; at most one heap per exiting thread can leak, in a rare destructor order). See `my_modules/rpmalloc/module.jai`.
|
||||||
|
|
||||||
|
## 11. Before you switch a project
|
||||||
|
|
||||||
|
- Check every `RESIZE` / `realloc` call for a correct `old_size` (§3). Jai's module ignores it; this one relies on it.
|
||||||
|
- Expect a larger footprint on Windows for large and huge blocks, or call `trim()` at quiet points.
|
||||||
|
- Workloads with many huge blocks (2 MiB and up) are slower than with Jai's module (§9).
|
||||||
|
- Supported platforms are Windows and macOS; Linux is type-checked only.
|
||||||
|
- A block must still be freed by the allocator that made it. Each program and DLL compiled with this module has its own instance, just as with Jai's.
|
||||||
21
modules/Default_Allocator/LICENSE
Normal file
21
modules/Default_Allocator/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Mattias Jansson
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
21
modules/Default_Allocator/LICENSES/MIT.txt
Normal file
21
modules/Default_Allocator/LICENSES/MIT.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Mattias Jansson
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
24
modules/Default_Allocator/LICENSES/Unlicense.txt
Normal file
24
modules/Default_Allocator/LICENSES/Unlicense.txt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <https://unlicense.org/>
|
||||||
59
modules/Default_Allocator/README.md
Normal file
59
modules/Default_Allocator/README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Default_Allocator (rpmalloc 2.0.1 port)
|
||||||
|
|
||||||
|
A drop-in replacement for Jai's `modules/Default_Allocator`: the rpmalloc 2.0.1 port, wrapped in the interface Jai's runtime expects. This directory holds only what differs from the port's other flavour. `install.jai` at the repository root puts it together with the shared allocator files.
|
||||||
|
|
||||||
|
## Installing it
|
||||||
|
|
||||||
|
From the repository root (`my_modules/rpmalloc`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jai install.jai - Default_Allocator PROJECT/modules
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the same command again to update the copy when the repository has a newer `VERSION_REVISION`. Add `check` to see what it would do. The repository's `README.md` describes every case.
|
||||||
|
|
||||||
|
## Using it
|
||||||
|
|
||||||
|
Jai picks its default allocator by module name: `Runtime_Support.jai` imports `"Default_Allocator"`. `PROJECT/modules` has to come ahead of Jai's own `modules` directory on the import path:
|
||||||
|
|
||||||
|
- Jai searches the working directory's `modules/` folder first, so building from the project's root is enough: `jai main.jai`.
|
||||||
|
- From elsewhere, or to be explicit: `jai main.jai -import_dir PROJECT/modules`, or in a build program prepend the directory to `Build_Options.import_path`:
|
||||||
|
|
||||||
|
```jai
|
||||||
|
paths: [..] string;
|
||||||
|
array_add(*paths, "PROJECT/modules");
|
||||||
|
array_add(*paths, ..options.import_path);
|
||||||
|
options.import_path = paths;
|
||||||
|
```
|
||||||
|
|
||||||
|
Every allocation the program makes then goes through this allocator, from the first one, and at compile time too. Nothing needs to set `context.allocator`.
|
||||||
|
|
||||||
|
**Thread pools:** call `Default_Allocator.thread_idle()` where a pool thread waits for work. Each thread has its own heap, and an idle thread makes no allocator calls, so without it the memory other threads free into that heap stays committed until the thread wakes (DIFFERENCES.md, section 4).
|
||||||
|
|
||||||
|
To check which allocator, and which version, a program is using:
|
||||||
|
|
||||||
|
```jai
|
||||||
|
name: string;
|
||||||
|
context.allocator.proc(.CAPS, 0, 0, *name, context.allocator.data);
|
||||||
|
// "rpmalloc 2.0.1-jai.N (Jai port) as Default_Allocator", N being VERSION_REVISION
|
||||||
|
```
|
||||||
|
|
||||||
|
`Default_Allocator.VERSION` holds the same version (`version.jai` in the installed copy).
|
||||||
|
|
||||||
|
**Supported:** 64-bit Windows and macOS (tested), and Linux (type-checked only). Other targets stop with an `#assert`; build them without this module on the import path.
|
||||||
|
|
||||||
|
**Read [DIFFERENCES.md](DIFFERENCES.md) before switching a project.** In particular, RESIZE relies on `old_size`, memory retention differs, and huge-block churn is slower.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | In the repository | Installed copy |
|
||||||
|
|---|---|---|
|
||||||
|
| `module.jai` | `Default_Allocator/module.jai`: Jai's `Default_Allocator` interface (`allocator`, `allocator_proc`, `unsafe_reset`, `initialize_if_needed`, …) and the port's public types | `module.jai` |
|
||||||
|
| `README.md`, `DIFFERENCES.md` | `Default_Allocator/` | the same names |
|
||||||
|
| `rpmalloc.jai`, `os_windows.jai`, `os_posix.jai`, `version.jai`, `LICENSE`, `LICENSES/` | the repository root, shared with the `rpmalloc2` flavour | the same names |
|
||||||
|
|
||||||
|
`module.jai` loads the shared files from its own directory, so it builds only in an installed copy. In the repository, `tests/` checks it with `jai build.jai - swap`, which installs this flavour into a scratch directory and runs a program on it.
|
||||||
|
|
||||||
|
When the port's `module.jai` changes the protocol procedure or the public types, bring this `module.jai` up to date by hand. It has its own copy of both.
|
||||||
|
|
||||||
|
License: rpmalloc is dual licensed Unlicense / MIT (`LICENSE`, `LICENSES/`).
|
||||||
303
modules/Default_Allocator/module.jai
Normal file
303
modules/Default_Allocator/module.jai
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
// Default_Allocator: rpmalloc 2.0.1 for Jai, as a drop-in replacement for Jai's modules/Default_Allocator.
|
||||||
|
//
|
||||||
|
// Jai finds the default allocator by module name: Runtime_Support.jai sets the default context's
|
||||||
|
// allocator to Default_Allocator.allocator_proc, and Process/posix.jai calls
|
||||||
|
// Default_Allocator.unsafe_reset() and allocator_proc(.STARTUP) in a forked child. Put the directory
|
||||||
|
// that holds this module ahead of Jai's modules directory on the import path, and every allocation
|
||||||
|
// of the program, from the first one the runtime makes, goes through this allocator.
|
||||||
|
//
|
||||||
|
// The allocator is the port in my_modules/rpmalloc (tested by its tests/ suite). This file is the
|
||||||
|
// repository's Default_Allocator/module.jai; install.jai copies it next to the port's shared files
|
||||||
|
// (rpmalloc.jai, os_*.jai, version.jai) in PROJECT/modules/Default_Allocator, and only there does it
|
||||||
|
// build. Upstream is dual licensed Unlicense / MIT; see LICENSE and LICENSES/.
|
||||||
|
//
|
||||||
|
// Read DIFFERENCES.md before switching a project over: it lists everything that behaves differently
|
||||||
|
// from Jai's Default_Allocator.
|
||||||
|
|
||||||
|
#module_parameters (
|
||||||
|
// The first two are Jai's Default_Allocator parameters, with the same names and defaults.
|
||||||
|
|
||||||
|
// Internal consistency asserts, reported through Memory_Interface.error_callback if set,
|
||||||
|
// otherwise context.assertion_failed. Upstream rpmalloc default: off.
|
||||||
|
ENABLE_ASSERTS := true,
|
||||||
|
// Refuse bad arguments at the public entry points: sizes at or above MAX_ALLOC_SIZE (negative
|
||||||
|
// sizes included), count * size overflow in the calloc family, size + alignment overflow and
|
||||||
|
// non-power-of-two alignments. Upstream rpmalloc default: off.
|
||||||
|
ENABLE_VALIDATE_ARGS := true,
|
||||||
|
|
||||||
|
// The rest are the port's (see my_modules/rpmalloc/module.jai). Runtime_Support imports this
|
||||||
|
// module without parameters, so the defaults below are what a program's context allocator uses.
|
||||||
|
|
||||||
|
// Global, per-thread and (with HEAP_STATISTICS) per-heap counters: global_statistics,
|
||||||
|
// thread_statistics, dump_statistics, heap_statistics.
|
||||||
|
ENABLE_STATISTICS := false,
|
||||||
|
// At finalize, assert that every block was freed. Needs ENABLE_STATISTICS.
|
||||||
|
ENABLE_LEAK_DETECTION := false,
|
||||||
|
// Commit pages on demand, and decommit free pages beyond a per page type threshold.
|
||||||
|
ENABLE_DECOMMIT := true,
|
||||||
|
// Return address space to the OS (huge blocks; everything at finalize with unmap_on_finalize).
|
||||||
|
ENABLE_UNMAP := true,
|
||||||
|
// The heap_* API and Allocator_Mode.CREATE_HEAP / DESTROY_HEAP.
|
||||||
|
FIRST_CLASS_HEAPS := true,
|
||||||
|
// Per first-class heap allocated / committed / mapped sizes (heap_statistics).
|
||||||
|
HEAP_STATISTICS := false,
|
||||||
|
// Recently freed huge blocks kept for reuse (at most 128 MiB, 32 MiB each); 0 disables the cache.
|
||||||
|
HUGE_CACHE_SLOT_COUNT := 32,
|
||||||
|
// How much freed huge-block memory the cache may keep committed, in MiB; a single entry may take a
|
||||||
|
// quarter of it. Upstream's 128. A program that frees and allocates huge blocks often runs faster
|
||||||
|
// with more (churn_huge misses the cache on 26% of steps at 128, 5% at 256, 1% at 512), at the cost of
|
||||||
|
// up to this much memory held while it does; entries idle for a second are decommitted (#jai fix E11).
|
||||||
|
HUGE_CACHE_LIMIT_MB := 128,
|
||||||
|
// Keep the thread heap in the Jai Context (field rpmalloc_default_allocator_thread_heap) as well as in OS thread-local storage, so
|
||||||
|
// allocations and frees need no OS call to find it (#jai fix E10). Two imports of this module cannot
|
||||||
|
// both add the field: turn it off for a second import with other parameters.
|
||||||
|
CONTEXT_HEAP_CACHE := true,
|
||||||
|
// Export internals and fault injection for the port's test suite. Adds exports only.
|
||||||
|
TEST_HOOKS := false
|
||||||
|
)();
|
||||||
|
|
||||||
|
#assert !ENABLE_LEAK_DETECTION || ENABLE_STATISTICS "ENABLE_LEAK_DETECTION needs ENABLE_STATISTICS";
|
||||||
|
|
||||||
|
// #jai Jai's Default_Allocator also builds for PS5, Switch 2, iOS, Android and 32-bit targets (as a
|
||||||
|
// CRT malloc wrapper on most of them). The port has an OS layer for Windows, macOS and Linux only.
|
||||||
|
#assert (OS == .WINDOWS || OS == .MACOS || OS == .LINUX) && (CPU == .X64 || CPU == .ARM64)
|
||||||
|
"This Default_Allocator (rpmalloc 2.0.1 port) supports 64-bit Windows, macOS and Linux only. For other targets, take my_modules off the import path so Jai's own Default_Allocator is used.";
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Jai's Default_Allocator interface, as Runtime_Support, Process and other code use it:
|
||||||
|
|
||||||
|
allocator Allocator.{allocator_proc, null}
|
||||||
|
allocator_proc The Allocator protocol, below.
|
||||||
|
unsafe_reset() For a forked child only, before it allocates.
|
||||||
|
initialize_if_needed() Initialise now; true if this call did it.
|
||||||
|
is_initialized()
|
||||||
|
finalize()
|
||||||
|
usable_size(ptr)
|
||||||
|
is_this_yours(heap, ptr)
|
||||||
|
heap_free_all(heap)
|
||||||
|
global_statistics() Also global_statistics(*stats), the rpmalloc form.
|
||||||
|
heap_statistics(heap)
|
||||||
|
|
||||||
|
Not provided: allocator_default_heap. There is no single default heap; each thread has its own.
|
||||||
|
|
||||||
|
Allocator protocol (allocator_proc):
|
||||||
|
|
||||||
|
ALLOCATE, RESIZE, FREE The calling thread's heap, or the first-class heap in allocator_data.
|
||||||
|
Out of memory returns null. With ENABLE_VALIDATE_ARGS, so does an
|
||||||
|
impossible size (negative or near S64_MAX), and such a RESIZE leaves
|
||||||
|
the block intact. FREE of null is a no-op. RESIZE copies old_size
|
||||||
|
bytes when the block moves (all of the old block when old_size is 0).
|
||||||
|
STARTUP Optional: initialize(). Returns null.
|
||||||
|
SHUTDOWN finalize(). Allocating afterwards initialises again.
|
||||||
|
THREAD_START, THREAD_STOP thread_initialize(), thread_finalize(). Both optional: a thread gets
|
||||||
|
its heap on its first allocation, and gives it back at thread exit.
|
||||||
|
CREATE_HEAP heap_acquire(): a first-class heap, to put in allocator_data.
|
||||||
|
DESTROY_HEAP Frees every block still in the heap, then releases it.
|
||||||
|
IS_THIS_YOURS With a heap: is the address inside a page of that heap? Without:
|
||||||
|
inside a page of any thread heap (not of a first-class heap)?
|
||||||
|
CAPS Everything above; CREATE_HEAP only with FIRST_CLASS_HEAPS.
|
||||||
|
|
||||||
|
For a thread pool: call thread_idle() where a pool thread waits for work. It gives back what
|
||||||
|
the thread's heap holds free and parks the heap, so blocks other threads free into it while it
|
||||||
|
sleeps are given back at once; the thread's next allocation takes the heap back.
|
||||||
|
|
||||||
|
The whole rpmalloc API of the port is exported as well: initialize, thread_collect, trim, thread_idle,
|
||||||
|
rpmalloc, rpaligned_alloc, heap_acquire, and the rest (my_modules/rpmalloc/module.jai lists it).
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
allocator :: Allocator.{allocator_proc, null};
|
||||||
|
|
||||||
|
allocator_proc :: (mode: Allocator_Mode, requested_size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
|
||||||
|
heap := cast(*Heap) allocator_data;
|
||||||
|
|
||||||
|
if #complete mode == {
|
||||||
|
case .ALLOCATE;
|
||||||
|
#if FIRST_CLASS_HEAPS if heap return heap_alloc(heap, requested_size);
|
||||||
|
return rpmalloc(requested_size);
|
||||||
|
|
||||||
|
case .RESIZE;
|
||||||
|
return allocator_proc_resize(heap, old_memory, requested_size, old_size);
|
||||||
|
|
||||||
|
case .FREE;
|
||||||
|
#if FIRST_CLASS_HEAPS if heap { heap_free(heap, old_memory); return null; } // Null is a no-op (#jai fix U18).
|
||||||
|
rpfree(old_memory);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case .STARTUP;
|
||||||
|
initialize();
|
||||||
|
return null; // Process/posix.jai stores this in context.allocator.data: null means the thread heap.
|
||||||
|
|
||||||
|
case .SHUTDOWN;
|
||||||
|
finalize();
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case .THREAD_START;
|
||||||
|
thread_initialize();
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case .THREAD_STOP;
|
||||||
|
thread_finalize();
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case .CREATE_HEAP;
|
||||||
|
#if FIRST_CLASS_HEAPS return heap_acquire();
|
||||||
|
else return null;
|
||||||
|
|
||||||
|
case .DESTROY_HEAP;
|
||||||
|
// #jai fix M4: destroying a heap frees every block still in it.
|
||||||
|
#if FIRST_CLASS_HEAPS if heap {
|
||||||
|
heap_free_all(heap);
|
||||||
|
heap_release(heap);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case .IS_THIS_YOURS;
|
||||||
|
return cast(*void) cast(s64) is_this_yours(heap, old_memory);
|
||||||
|
|
||||||
|
case .CAPS;
|
||||||
|
if old_memory { (cast(*string) old_memory).* = CAPS_VERSION_STRING; }
|
||||||
|
caps := Allocator_Caps.HINT_I_AM_A_GENERAL_HEAP_ALLOCATOR | .MULTIPLE_THREADS | .FREE | .ACTUALLY_RESIZE | .IS_THIS_YOURS;
|
||||||
|
#if FIRST_CLASS_HEAPS caps |= .CREATE_HEAP;
|
||||||
|
return cast(*void) caps;
|
||||||
|
|
||||||
|
case;
|
||||||
|
context.handling_assertion_failure = true;
|
||||||
|
context.assertion_failed(#location(), "Invalid or corrupt mode passed to Default_Allocator.allocator_proc.\n");
|
||||||
|
context.handling_assertion_failure = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jai's Default_Allocator interface: initialise now, unless already done. True if this call did it.
|
||||||
|
initialize_if_needed :: () -> did_init: bool {
|
||||||
|
if is_initialized() return false;
|
||||||
|
initialize();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_initialized :: inline () -> bool {
|
||||||
|
return atomic_load(*global_rpmalloc_init_state) == RPMALLOC_INIT_DONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jai's Default_Allocator form of global_statistics. With ENABLE_STATISTICS off (the default) every
|
||||||
|
// field is 0, as with Jai's.
|
||||||
|
global_statistics :: () -> Global_Statistics {
|
||||||
|
stats: Global_Statistics;
|
||||||
|
global_statistics(*stats);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// :AllocatorAfterFork: Only for the child of fork(), before it allocates (Process/posix.jai). The
|
||||||
|
// child has one thread, the one that called fork; any lock another thread of the parent held at that
|
||||||
|
// moment would never be released, so the next allocation would wait forever. This releases them.
|
||||||
|
//
|
||||||
|
// #jai Jai's Default_Allocator forgets its whole state here and starts afresh. This keeps it: the
|
||||||
|
// forking thread's heap and every block it holds stay valid, and the heaps of the parent's other
|
||||||
|
// threads stay allocated (in the child nobody frees them, as with fork and any allocator). Like Jai's,
|
||||||
|
// it cannot repair a list another thread was in the middle of changing; the window is a few
|
||||||
|
// instructions under the global heap lock or the huge cache lock.
|
||||||
|
unsafe_reset :: () {
|
||||||
|
atomic_store(*global_heap_lock, 0);
|
||||||
|
global_heap_creating = 0; // Another thread was mapping a block of heaps; the child would wait for it.
|
||||||
|
#if HUGE_CACHE_SLOT_COUNT atomic_store(*global_huge_cache.lock, 0);
|
||||||
|
// Another thread was initialising: start again (the forking thread was not, it was in fork).
|
||||||
|
if atomic_load(*global_rpmalloc_init_state) == RPMALLOC_INIT_RUNNING
|
||||||
|
atomic_store(*global_rpmalloc_init_state, RPMALLOC_INIT_UNINIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public types (2.0.1: rpmalloc.h). Kept identical to my_modules/rpmalloc/module.jai.
|
||||||
|
//
|
||||||
|
|
||||||
|
#load "version.jai"; // VERSION, VERSION_REVISION, RPMALLOC_VERSION
|
||||||
|
|
||||||
|
RPMALLOC_MAX_ALIGNMENT :: 256 * 1024; // 2.0.1: rpmalloc.h:69
|
||||||
|
|
||||||
|
// Flags for rpaligned_realloc / heap_realloc (2.0.1: rpmalloc.h:79-84).
|
||||||
|
Realloc_Flags :: enum_flags u32 {
|
||||||
|
NO_PRESERVE :: 1; // Do not preserve content in reallocation.
|
||||||
|
GROW_OR_FAIL :: 2; // Fail (return null) if the block cannot grow in place; the original stays valid.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.0.1: rpmalloc.h rpmalloc_interface_t. Pass one to initialize / initialize_config to replace
|
||||||
|
// the OS calls; it is copied, so later changes to it have no effect. If memory_map or memory_unmap
|
||||||
|
// is null, all four OS defaults are used; otherwise a null memory_commit or memory_decommit uses
|
||||||
|
// the OS's (#jai fix U8).
|
||||||
|
Memory_Interface :: struct {
|
||||||
|
// Map `size` bytes aligned to `alignment` (0 or the span size). May store an alignment offset
|
||||||
|
// and the mapped size, both passed back to memory_unmap.
|
||||||
|
memory_map: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void;
|
||||||
|
memory_commit: (address: *void, size: u64) -> s32; // Non-zero on failure.
|
||||||
|
memory_decommit: (address: *void, size: u64) -> s32; // Non-zero on failure.
|
||||||
|
memory_unmap: (address: *void, offset: u64, mapped_size: u64);
|
||||||
|
map_fail_callback: (size: u64) -> s32; // Non-zero: retry the map.
|
||||||
|
error_callback: (message: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.0.1: rpmalloc.h rpmalloc_config_t.
|
||||||
|
Config :: struct {
|
||||||
|
page_size: u64; // 0: the OS page size.
|
||||||
|
enable_huge_pages: bool;
|
||||||
|
enable_thp: bool; // Linux / Android only.
|
||||||
|
disable_decommit: bool;
|
||||||
|
page_name: string;
|
||||||
|
huge_page_name: string;
|
||||||
|
unmap_on_finalize: bool;
|
||||||
|
disable_thp: bool; // Linux / Android only.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.0.1: rpmalloc.h rpmalloc_global_statistics_t. All zero without ENABLE_STATISTICS. The memory
|
||||||
|
// committed now is active, not committed: committed and decommitted are running totals, as upstream.
|
||||||
|
Global_Statistics :: struct {
|
||||||
|
mapped: u64; // Address space mapped now, committed or not.
|
||||||
|
mapped_peak: u64;
|
||||||
|
committed: u64; // Running total of bytes ever committed; it only grows.
|
||||||
|
decommitted: u64; // Running total of bytes ever decommitted; it only grows.
|
||||||
|
active: u64; // Bytes committed now: what the OS counts (API-07 checks it).
|
||||||
|
active_peak: u64;
|
||||||
|
huge_alloc: u64; // Bytes in huge blocks allocated now.
|
||||||
|
huge_alloc_peak: u64;
|
||||||
|
heap_count: u64; // Heaps in use now.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.0.1: rpmalloc.h rpmalloc_thread_statistics_t.
|
||||||
|
Thread_Statistics :: struct {
|
||||||
|
sizecache: u64; // Bytes available in thread size class caches.
|
||||||
|
spancache: u64; // Bytes available in thread page caches.
|
||||||
|
span_use: [5] struct {
|
||||||
|
current: u64;
|
||||||
|
map_calls: u64;
|
||||||
|
};
|
||||||
|
size_use: [128] struct {
|
||||||
|
alloc_current: u64;
|
||||||
|
alloc_peak: u64;
|
||||||
|
alloc_total: u64;
|
||||||
|
free_total: u64;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.0.1: rpmalloc.h rpmalloc_heap_statistics_t.
|
||||||
|
Heap_Statistics :: struct {
|
||||||
|
allocated_size: u64;
|
||||||
|
committed_size: u64;
|
||||||
|
mapped_size: u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scope_module
|
||||||
|
|
||||||
|
// #jai fix E10: the thread heap, cached in the Context (see get_thread_heap in rpmalloc.jai).
|
||||||
|
#if CONTEXT_HEAP_CACHE {
|
||||||
|
#add_context rpmalloc_default_allocator_thread_heap: *void;
|
||||||
|
context_thread_heap :: () -> **void #expand { return *context.rpmalloc_default_allocator_thread_heap; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// A literal, not built from VERSION with #run: a #run would wait for the default allocator, which is
|
||||||
|
// this module, whose CAPS needs this string. The assert keeps it in step with version.jai.
|
||||||
|
CAPS_VERSION_STRING :: "rpmalloc 2.0.1-jai.16 (Jai port) as Default_Allocator";
|
||||||
|
#assert #run text_is(CAPS_VERSION_STRING, "rpmalloc ", VERSION, " (Jai port) as Default_Allocator") "Update CAPS_VERSION_STRING to the VERSION in version.jai";
|
||||||
|
|
||||||
|
#load "rpmalloc.jai";
|
||||||
|
#if OS == .WINDOWS #load "os_windows.jai";
|
||||||
|
else #load "os_posix.jai";
|
||||||
408
modules/Default_Allocator/os_posix.jai
Normal file
408
modules/Default_Allocator/os_posix.jai
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
// OS entry points for macOS and Linux (2.0.1: rpmalloc.c:878-1161, the POSIX branches), plus
|
||||||
|
// the POSIX part of rpmalloc_initialize (2.0.1: rpmalloc.c:2776-2782).
|
||||||
|
//
|
||||||
|
// Everything goes through libc on every CPU. The 1.4.4 Jai port's raw syscalls caused review
|
||||||
|
// findings H4, H7 and M1; none of that code exists here.
|
||||||
|
//
|
||||||
|
// #jai deliberate differences from upstream:
|
||||||
|
// - A failed map returns null without asserting (finding N1).
|
||||||
|
// - macOS decommits with MADV_FREE_REUSABLE, retried on EAGAIN, and commits with
|
||||||
|
// MADV_FREE_REUSE. Upstream prefers MADV_DONTNEED, which exists on macOS but does not reduce
|
||||||
|
// the process footprint there, so decommit is a no-op on macOS (upstream bug U21).
|
||||||
|
// - Every entry point works before initialize(), like os_windows.jai.
|
||||||
|
|
||||||
|
// Fills os_map_granularity and os_page_size. Idempotent.
|
||||||
|
// 2.0.1: rpmalloc.c:2776-2782 (the POSIX part of rpmalloc_initialize)
|
||||||
|
os_initialize :: () {
|
||||||
|
if os_page_size return;
|
||||||
|
os_map_granularity = cast(u64) sysconf_p(_SC_PAGESIZE);
|
||||||
|
os_page_size = os_map_granularity;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_set_page_name :: (address: *void, size: u64) { // 2.0.1: rpmalloc.c:885
|
||||||
|
#if OS == .LINUX {
|
||||||
|
name := ifx os_huge_pages then global_config.huge_page_name else global_config.page_name;
|
||||||
|
if address == MAP_FAILED_P || !name return;
|
||||||
|
// prctl wants a C string; names are short, copy into a terminated buffer.
|
||||||
|
buffer: [80] u8;
|
||||||
|
n := min(name.count, buffer.count - 1);
|
||||||
|
memcpy(buffer.data, name.data, n);
|
||||||
|
buffer[n] = 0;
|
||||||
|
// If the kernel lacks CONFIG_ANON_VMA_NAME, or the name is invalid, this does nothing.
|
||||||
|
prctl_p(PR_SET_VMA, PR_SET_VMA_ANON_NAME, cast(u64) address, size, cast(u64) buffer.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #jai fix E9: a mapping that is committed as well. On POSIX a fresh anonymous mapping is usable as it
|
||||||
|
// is, so this only counts the commit (no madvise); see os_windows.jai for why huge blocks use it.
|
||||||
|
os_mmap_committed :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void {
|
||||||
|
ptr := os_mmap(size, alignment, offset, mapped_size);
|
||||||
|
#if ENABLE_DECOMMIT if ptr statistics_os_commit(size);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_mmap :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void { // 2.0.1: rpmalloc.c:900
|
||||||
|
os_initialize();
|
||||||
|
#if TEST_HOOKS if test_take_fault(*test_fail_maps) return os_map_failed(size, alignment, offset, mapped_size);
|
||||||
|
|
||||||
|
map_size := size + alignment; // UPSTREAM-BUG U1: wraps for sizes near U64_MAX.
|
||||||
|
flags: s32 = MAP_PRIVATE_P | MAP_ANONYMOUS_P | MAP_UNINITIALIZED_P;
|
||||||
|
#if OS == .MACOS {
|
||||||
|
fd := VM_MAKE_TAG_240;
|
||||||
|
if os_huge_pages fd |= VM_FLAGS_SUPERPAGE_SIZE_2MB;
|
||||||
|
ptr := mmap_p(null, map_size, PROT_READ_WRITE_P, flags, fd, 0);
|
||||||
|
if (ptr == MAP_FAILED_P || !ptr) && os_huge_pages {
|
||||||
|
// Superpage allocations can fail through physical memory fragmentation; fall back
|
||||||
|
// to regular pages.
|
||||||
|
ptr = mmap_p(null, map_size, PROT_READ_WRITE_P, flags, VM_MAKE_TAG_240, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
huge_flags: s32 = 0;
|
||||||
|
if os_huge_pages {
|
||||||
|
huge_flags = MAP_HUGETLB_P;
|
||||||
|
// Explicitly request the detected huge page size; the system default huge page size
|
||||||
|
// can be a different (unusable) size like 1 GiB.
|
||||||
|
if os_huge_page_shift huge_flags |= cast(s32)(os_huge_page_shift << MAP_HUGE_SHIFT_P);
|
||||||
|
}
|
||||||
|
ptr := mmap_p(null, map_size, PROT_READ_WRITE_P, huge_flags | flags, -1, 0);
|
||||||
|
if (ptr == MAP_FAILED_P || !ptr) && os_huge_pages {
|
||||||
|
// Huge page allocations can fail: fall back to normal pages and advise transparent
|
||||||
|
// huge pages (best effort; for example EINVAL when THP is disabled system-wide).
|
||||||
|
ptr = mmap_p(null, map_size, PROT_READ_WRITE_P, flags, -1, 0);
|
||||||
|
if ptr && ptr != MAP_FAILED_P madvise_p(ptr, map_size, MADV_HUGEPAGE_P);
|
||||||
|
} else if os_thp && ptr && ptr != MAP_FAILED_P {
|
||||||
|
// Transparent huge page mode: ask for huge pages without a preallocated pool.
|
||||||
|
madvise_p(ptr, map_size, MADV_HUGEPAGE_P);
|
||||||
|
}
|
||||||
|
os_set_page_name(ptr, map_size);
|
||||||
|
}
|
||||||
|
if ptr == MAP_FAILED_P ptr = null;
|
||||||
|
if !ptr return os_map_failed(size, alignment, offset, mapped_size);
|
||||||
|
|
||||||
|
if alignment {
|
||||||
|
padding := cast(u64) ptr & (alignment - 1);
|
||||||
|
if padding padding = alignment - padding;
|
||||||
|
rpmalloc_assert(padding <= alignment, "Internal failure in padding");
|
||||||
|
rpmalloc_assert(!(padding % 8), "Internal failure in padding");
|
||||||
|
// Unmap the alignment padding head and tail, so no unusable address space (or huge page
|
||||||
|
// reservation) is kept. Both the mmap result and the alignment are multiples of the
|
||||||
|
// mapping page size, so head and tail stay page aligned.
|
||||||
|
if padding munmap_p(ptr, padding);
|
||||||
|
ptr = pointer_offset(ptr, padding);
|
||||||
|
if alignment - padding munmap_p(pointer_offset(ptr, size), alignment - padding);
|
||||||
|
offset.* = 0;
|
||||||
|
map_size = size;
|
||||||
|
}
|
||||||
|
mapped_size.* = map_size;
|
||||||
|
statistics_os_map(map_size); // 2.0.1: rpmalloc.c:1023-1033
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The failure tail of os_mmap (2.0.1: rpmalloc.c:990-998).
|
||||||
|
// #jai returns null without asserting when there is no map_fail_callback (finding N1).
|
||||||
|
os_map_failed :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void {
|
||||||
|
iface := global_memory_interface;
|
||||||
|
if iface && iface.map_fail_callback && iface.map_fail_callback(size + alignment)
|
||||||
|
return os_mmap(size, alignment, offset, mapped_size);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_commit_on_demand :: inline () -> bool { // 2.0.1: rpmalloc.c:1040
|
||||||
|
#if ENABLE_DECOMMIT return !global_config.disable_decommit;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_mcommit :: (address: *void, size: u64) -> s32 { // 2.0.1: rpmalloc.c:1049
|
||||||
|
#if ENABLE_DECOMMIT {
|
||||||
|
#if TEST_HOOKS if test_take_fault(*test_fail_commits) return 1;
|
||||||
|
// POSIX commits on first touch, so upstream does nothing here (its mprotect is commented
|
||||||
|
// out). #jai on macOS, tell the kernel the pages given back with MADV_FREE_REUSABLE are
|
||||||
|
// in use again, so the footprint accounting stays right (the pairing Apple documents).
|
||||||
|
#if OS == .MACOS madvise_p(address, size, MADV_FREE_REUSE_P);
|
||||||
|
statistics_os_commit(size); // 2.0.1: rpmalloc.c:1067-1078
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_mdecommit :: (address: *void, size: u64) -> s32 { // 2.0.1: rpmalloc.c:1086
|
||||||
|
#if ENABLE_DECOMMIT {
|
||||||
|
// Unlike os_mcommit, honor the live flag (the Linux fallback below can set it mid-run).
|
||||||
|
if global_config.disable_decommit return 1;
|
||||||
|
#if OS == .MACOS {
|
||||||
|
// #jai MADV_FREE_REUSABLE, not MADV_DONTNEED (upstream bug U21).
|
||||||
|
ret := madvise_p(address, size, MADV_FREE_REUSABLE_P);
|
||||||
|
while ret == -1 && errno_p().* == EAGAIN_P {
|
||||||
|
errno_p().* = 0;
|
||||||
|
ret = madvise_p(address, size, MADV_FREE_REUSABLE_P);
|
||||||
|
}
|
||||||
|
failed := ret == -1;
|
||||||
|
} else {
|
||||||
|
failed := madvise_p(address, size, MADV_DONTNEED_P) != 0;
|
||||||
|
if failed && os_huge_pages {
|
||||||
|
// MADV_DONTNEED on huge page mappings needs Linux 5.18 or later: keep pages
|
||||||
|
// committed and stop further decommit attempts.
|
||||||
|
global_config.disable_decommit = true;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
rpmalloc_assert(false, "Failed to decommit virtual memory block");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
statistics_os_decommit(size); // 2.0.1: rpmalloc.c:1129-1133
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_munmap :: (address: *void, offset: u64, mapped_size: u64) { // 2.0.1: rpmalloc.c:1142
|
||||||
|
address = pointer_offset(address, -cast(s64) offset);
|
||||||
|
#if ENABLE_UNMAP {
|
||||||
|
if munmap_p(address, mapped_size)
|
||||||
|
rpmalloc_assert(false, "Failed to unmap virtual memory block");
|
||||||
|
statistics_os_unmap(mapped_size); // 2.0.1: rpmalloc.c:1154-1158
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os_thread_yield :: () { // 2.0.1: rpmalloc.c:396 (sched_yield)
|
||||||
|
sched_yield_p();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coarse monotonic time for the huge block cache.
|
||||||
|
os_monotonic_time_ms :: () -> u64 { // 2.0.1: rpmalloc.c:1586
|
||||||
|
ts: timespec_p;
|
||||||
|
clock_gettime_p(CLOCK_MONOTONIC_FOR_CACHE, *ts);
|
||||||
|
return cast(u64) ts.tv_sec * 1000 + cast(u64) ts.tv_nsec / 1_000_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Thread-local heap storage, thread identity and the thread exit hook.
|
||||||
|
//
|
||||||
|
// #jai upstream keeps the thread heap in a C11 _Thread_local variable and uses a pthread key only
|
||||||
|
// for the thread exit destructor. This Jai version has no #thread_local, and keeping the heap in
|
||||||
|
// the Context would let copied contexts share a heap between threads (review H3), so one pthread
|
||||||
|
// key does both: it stores the heap pointer and runs the destructor at thread exit. The destructor
|
||||||
|
// matters more here than upstream: Jai's Thread module never sends THREAD_STOP on POSIX (finding
|
||||||
|
// N4), so it is what returns a finished Jai thread's heap.
|
||||||
|
|
||||||
|
os_tls_create :: (destructor: (value: *void) #c_call) -> bool {
|
||||||
|
return pthread_key_create_p(*tls_key, destructor) == 0; // 2.0.1: rpmalloc.c:2954
|
||||||
|
}
|
||||||
|
|
||||||
|
os_tls_destroy :: () {
|
||||||
|
pthread_key_delete_p(tls_key); // 2.0.1: rpmalloc.c:3102
|
||||||
|
tls_key = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #jai fix E10: the calling thread's stack, [low, high); 0, 0 if it cannot be found out (the Context
|
||||||
|
// cache of the thread heap is then never used).
|
||||||
|
os_thread_stack_range :: () -> low: u64, high: u64 {
|
||||||
|
self := pthread_self_p();
|
||||||
|
#if OS == .MACOS {
|
||||||
|
high := cast(u64) pthread_get_stackaddr_np_p(self); // The top: the stack grows down.
|
||||||
|
size := pthread_get_stacksize_np_p(self);
|
||||||
|
if !high || size > high return 0, 0;
|
||||||
|
return high - size, high;
|
||||||
|
} else {
|
||||||
|
attr: [8] u64; // pthread_attr_t: 56 bytes on x86-64, 64 on ARM64 glibc.
|
||||||
|
if pthread_getattr_np_p(self, attr.data) != 0 return 0, 0;
|
||||||
|
address: *void;
|
||||||
|
size: u64;
|
||||||
|
ok := pthread_attr_getstack_p(attr.data, *address, *size) == 0;
|
||||||
|
pthread_attr_destroy_p(attr.data);
|
||||||
|
if !ok return 0, 0;
|
||||||
|
return cast(u64) address, cast(u64) address + size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os_tls_get :: inline () -> *void {
|
||||||
|
return pthread_getspecific_p(tls_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
os_tls_set :: (value: *void) {
|
||||||
|
pthread_setspecific_p(tls_key, value); // 2.0.1: rpmalloc.c:805
|
||||||
|
}
|
||||||
|
|
||||||
|
// #jai upstream reads the thread pointer (__builtin_thread_pointer or fs/gs/tpidr registers,
|
||||||
|
// rpmalloc.c:763-790). pthread_self is equally unique among live threads and never 0.
|
||||||
|
// See os_windows.jai (#jai fix U22). POSIX exit() does not run other threads' key destructors, and
|
||||||
|
// the exiting thread's own run only from pthread_exit, so there is nothing to skip here.
|
||||||
|
os_process_exiting :: inline () -> bool { return false; }
|
||||||
|
|
||||||
|
os_thread_id :: inline () -> u64 {
|
||||||
|
return cast(u64) pthread_self_p();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Huge page detection (2.0.1: rpmalloc.c:2831-2892, the Linux and Apple branches).
|
||||||
|
os_detect_huge_pages :: () {
|
||||||
|
#if OS == .LINUX {
|
||||||
|
huge_page_size: u64 = 0;
|
||||||
|
meminfo: [4096] u8;
|
||||||
|
n := os_read_system_file("/proc/meminfo", meminfo.data, meminfo.count);
|
||||||
|
if n {
|
||||||
|
text := string.{n, meminfo.data};
|
||||||
|
at := find_substring(text, "Hugepagesize:");
|
||||||
|
if at >= 0 huge_page_size = parse_decimal(advance_string(text, at + 13)) * 1024;
|
||||||
|
}
|
||||||
|
huge_page_size = usable_huge_page_size(huge_page_size);
|
||||||
|
if huge_page_size {
|
||||||
|
// Hugepagesize only reports the configured size, not whether any huge pages are
|
||||||
|
// available. Probe by mapping one huge page with the flags real mappings use.
|
||||||
|
huge_shift: u64 = 0;
|
||||||
|
while (cast(u64) 1 << huge_shift) < huge_page_size huge_shift += 1;
|
||||||
|
probe_flags := MAP_PRIVATE_P | MAP_ANONYMOUS_P | MAP_HUGETLB_P | cast(s32)(huge_shift << MAP_HUGE_SHIFT_P);
|
||||||
|
probe := mmap_p(null, huge_page_size, PROT_READ_WRITE_P, probe_flags, -1, 0);
|
||||||
|
if probe != MAP_FAILED_P && probe {
|
||||||
|
munmap_p(probe, huge_page_size);
|
||||||
|
os_huge_pages = true;
|
||||||
|
os_huge_page_shift = huge_shift;
|
||||||
|
os_page_size = huge_page_size;
|
||||||
|
os_map_granularity = huge_page_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else #if OS == .MACOS && CPU == .X64 {
|
||||||
|
// Superpages are only supported on x86-64; Apple silicon has no huge page support.
|
||||||
|
os_huge_pages = true;
|
||||||
|
os_page_size = 2 * 1024 * 1024;
|
||||||
|
os_map_granularity = os_page_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transparent huge pages (2.0.1: rpmalloc.c:2921-2936, Linux only).
|
||||||
|
os_configure_thp :: () {
|
||||||
|
#if OS == .LINUX {
|
||||||
|
if global_config.enable_thp && !os_huge_pages && !global_config.disable_thp {
|
||||||
|
// With THP disabled system-wide ("never") madvise(MADV_HUGEPAGE) does nothing, so
|
||||||
|
// report THP as disabled rather than claim it is in use.
|
||||||
|
state: [64] u8;
|
||||||
|
n := os_read_system_file("/sys/kernel/mm/transparent_hugepage/enabled", state.data, state.count);
|
||||||
|
thp_never := n && find_substring(string.{n, state.data}, "[never]") >= 0;
|
||||||
|
if !thp_never os_thp = true;
|
||||||
|
}
|
||||||
|
if global_config.disable_thp prctl_p(PR_SET_THP_DISABLE_P, 1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if OS == .LINUX {
|
||||||
|
// Read a small (pseudo) file such as /proc/meminfo into a caller buffer with raw open/read, so
|
||||||
|
// nothing allocates during initialisation. Returns the bytes read (the buffer is always zero
|
||||||
|
// terminated), or 0.
|
||||||
|
os_read_system_file :: (path: string, buffer: *u8, capacity: s64) -> s64 { // 2.0.1: rpmalloc.c:2684
|
||||||
|
if capacity < 2 return 0;
|
||||||
|
fd := open_p(path.data, O_RDONLY_P | O_CLOEXEC_P); // path is a zero-terminated literal
|
||||||
|
if fd < 0 return 0;
|
||||||
|
total: s64 = 0;
|
||||||
|
while total + 1 < capacity {
|
||||||
|
got := read_p(fd, buffer + total, cast(u64)((capacity - 1) - total));
|
||||||
|
if got <= 0 break;
|
||||||
|
total += got;
|
||||||
|
}
|
||||||
|
close_p(fd);
|
||||||
|
buffer[total] = 0;
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
find_substring :: (text: string, pattern: string) -> s64 {
|
||||||
|
if pattern.count > text.count return -1;
|
||||||
|
for i: 0..text.count - pattern.count {
|
||||||
|
match := true;
|
||||||
|
for j: 0..pattern.count-1 if text[i + j] != pattern[j] { match = false; break; }
|
||||||
|
if match return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
advance_string :: (text: string, n: s64) -> string {
|
||||||
|
if n >= text.count return "";
|
||||||
|
return string.{text.count - n, text.data + n};
|
||||||
|
}
|
||||||
|
|
||||||
|
// strtol(..., 10) for a non-negative number: skips leading blanks, stops at the first non-digit.
|
||||||
|
parse_decimal :: (text: string) -> u64 {
|
||||||
|
i := 0;
|
||||||
|
while i < text.count && (text[i] == #char " " || text[i] == #char "\t") i += 1;
|
||||||
|
value: u64 = 0;
|
||||||
|
while i < text.count && text[i] >= #char "0" && text[i] <= #char "9" {
|
||||||
|
value = value * 10 + (text[i] - #char "0");
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#scope_file
|
||||||
|
|
||||||
|
libc_rpm :: #library,system "libc";
|
||||||
|
|
||||||
|
mmap_p :: (address: *void, length: u64, prot: s32, flags: s32, fd: s32, offset: s64) -> *void #foreign libc_rpm "mmap";
|
||||||
|
munmap_p :: (address: *void, length: u64) -> s32 #foreign libc_rpm "munmap";
|
||||||
|
madvise_p :: (address: *void, length: u64, advice: s32) -> s32 #foreign libc_rpm "madvise";
|
||||||
|
sysconf_p :: (name: s32) -> s64 #foreign libc_rpm "sysconf";
|
||||||
|
sched_yield_p :: () -> s32 #foreign libc_rpm "sched_yield";
|
||||||
|
|
||||||
|
timespec_p :: struct {
|
||||||
|
tv_sec: s64;
|
||||||
|
tv_nsec: s64;
|
||||||
|
}
|
||||||
|
clock_gettime_p :: (clock_id: s32, ts: *timespec_p) -> s32 #foreign libc_rpm "clock_gettime";
|
||||||
|
// 6 is CLOCK_MONOTONIC_COARSE on Linux (read through the vDSO, as upstream prefers) and
|
||||||
|
// CLOCK_MONOTONIC on macOS, which has no coarse clock.
|
||||||
|
CLOCK_MONOTONIC_FOR_CACHE :: 6;
|
||||||
|
|
||||||
|
MAP_FAILED_P :: cast(*void) -1;
|
||||||
|
PROT_READ_WRITE_P :: 0x1 | 0x2;
|
||||||
|
MAP_PRIVATE_P :: 0x0002;
|
||||||
|
|
||||||
|
#if OS == .MACOS {
|
||||||
|
errno_p :: () -> *s32 #foreign libc_rpm "__error";
|
||||||
|
MAP_ANONYMOUS_P :: 0x1000;
|
||||||
|
MAP_UNINITIALIZED_P :: 0;
|
||||||
|
_SC_PAGESIZE :: 29;
|
||||||
|
EAGAIN_P :: 35;
|
||||||
|
MADV_FREE_REUSABLE_P :: 7;
|
||||||
|
MADV_FREE_REUSE_P :: 8;
|
||||||
|
VM_MAKE_TAG_240 :: cast(s32)(240 << 24); // VM_MAKE_TAG(240U)
|
||||||
|
VM_FLAGS_SUPERPAGE_SIZE_2MB :: 2 << 16; // VM_FLAGS_SUPERPAGE_SIZE_2MB (x86-64 macOS)
|
||||||
|
} else {
|
||||||
|
prctl_p :: (option: s32, arg2: u64, arg3: u64, arg4: u64, arg5: u64) -> s32 #foreign libc_rpm "prctl";
|
||||||
|
MAP_ANONYMOUS_P :: 0x20;
|
||||||
|
MAP_UNINITIALIZED_P :: 0x400_0000; // Only honoured on no-MMU kernels.
|
||||||
|
MAP_HUGETLB_P :: 0x4_0000;
|
||||||
|
MAP_HUGE_SHIFT_P :: 26;
|
||||||
|
MADV_DONTNEED_P :: 4;
|
||||||
|
MADV_HUGEPAGE_P :: 14;
|
||||||
|
_SC_PAGESIZE :: 30;
|
||||||
|
PR_SET_VMA :: 0x5356_4d41;
|
||||||
|
PR_SET_VMA_ANON_NAME :: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread-local storage and huge page detection.
|
||||||
|
|
||||||
|
libc_tls :: #library,system "libc";
|
||||||
|
|
||||||
|
#if OS == .MACOS pthread_key_t_p :: u64; // unsigned long
|
||||||
|
else pthread_key_t_p :: u32; // unsigned int
|
||||||
|
|
||||||
|
tls_key: pthread_key_t_p;
|
||||||
|
|
||||||
|
pthread_key_create_p :: (key: *pthread_key_t_p, destructor: (value: *void) #c_call) -> s32 #foreign libc_tls "pthread_key_create";
|
||||||
|
pthread_key_delete_p :: (key: pthread_key_t_p) -> s32 #foreign libc_tls "pthread_key_delete";
|
||||||
|
pthread_getspecific_p :: (key: pthread_key_t_p) -> *void #foreign libc_tls "pthread_getspecific";
|
||||||
|
pthread_setspecific_p :: (key: pthread_key_t_p, value: *void) -> s32 #foreign libc_tls "pthread_setspecific";
|
||||||
|
pthread_self_p :: () -> *void #foreign libc_tls "pthread_self";
|
||||||
|
#if OS == .MACOS {
|
||||||
|
pthread_get_stackaddr_np_p :: (thread: *void) -> *void #foreign libc_tls "pthread_get_stackaddr_np";
|
||||||
|
pthread_get_stacksize_np_p :: (thread: *void) -> u64 #foreign libc_tls "pthread_get_stacksize_np";
|
||||||
|
} else {
|
||||||
|
pthread_getattr_np_p :: (thread: *void, attr: *u64) -> s32 #foreign libc_tls "pthread_getattr_np";
|
||||||
|
pthread_attr_getstack_p :: (attr: *u64, address: **void, size: *u64) -> s32 #foreign libc_tls "pthread_attr_getstack";
|
||||||
|
pthread_attr_destroy_p :: (attr: *u64) -> s32 #foreign libc_tls "pthread_attr_destroy";
|
||||||
|
}
|
||||||
|
|
||||||
|
#if OS == .LINUX {
|
||||||
|
open_p :: (path: *u8, flags: s32) -> s32 #foreign libc_tls "open";
|
||||||
|
read_p :: (fd: s32, buffer: *u8, count: u64) -> s64 #foreign libc_tls "read";
|
||||||
|
close_p :: (fd: s32) -> s32 #foreign libc_tls "close";
|
||||||
|
O_RDONLY_P :: 0;
|
||||||
|
O_CLOEXEC_P :: 0x8_0000;
|
||||||
|
PR_SET_THP_DISABLE_P :: 41;
|
||||||
|
}
|
||||||
334
modules/Default_Allocator/os_windows.jai
Normal file
334
modules/Default_Allocator/os_windows.jai
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
// OS entry points for Windows (2.0.1: rpmalloc.c:878-1161, the PLATFORM_WINDOWS branches),
|
||||||
|
// plus the Windows part of rpmalloc_initialize (2.0.1: rpmalloc.c:2765-2782).
|
||||||
|
//
|
||||||
|
// #jai deliberate differences from upstream, all in the error paths:
|
||||||
|
// - A failed map or commit returns null / non-zero without asserting (finding N1). Upstream
|
||||||
|
// asserts when no map_fail_callback is set and ENABLE_ASSERTS is on.
|
||||||
|
// - Every entry point works before initialize(): it initialises the OS globals on demand and
|
||||||
|
// tolerates a null global_memory_interface (used by the test hooks).
|
||||||
|
|
||||||
|
// Fills os_map_granularity, os_page_size and os_virtualalloc2. Idempotent.
|
||||||
|
// 2.0.1: rpmalloc.c:2765-2782 (the PLATFORM_WINDOWS part of rpmalloc_initialize)
|
||||||
|
os_initialize :: () {
|
||||||
|
if os_page_size return;
|
||||||
|
info: SYSTEM_INFO_W;
|
||||||
|
GetSystemInfo_w(*info);
|
||||||
|
os_map_granularity = info.dwAllocationGranularity;
|
||||||
|
os_virtualalloc2 = null;
|
||||||
|
// #jai GetModuleHandleA with an ASCII name; upstream uses the W variant. Same module.
|
||||||
|
kernelbase := GetModuleHandleA_w("kernelbase.dll".data);
|
||||||
|
if kernelbase os_virtualalloc2 = cast(VirtualAlloc2_Proc) GetProcAddress_w(kernelbase, "VirtualAlloc2".data);
|
||||||
|
os_page_size = info.dwPageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_set_page_name :: (address: *void, size: u64) { // 2.0.1: rpmalloc.c:885 (Linux only; a no-op here)
|
||||||
|
}
|
||||||
|
|
||||||
|
os_mmap :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void { // 2.0.1: rpmalloc.c:900
|
||||||
|
return os_map(size, alignment, offset, mapped_size, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #jai fix E9: a mapping that is committed as well, in the same call. Committing a range that is only
|
||||||
|
// reserved costs about 15 us per 24 MiB, and makes releasing it cost about 48 us instead of 7 us
|
||||||
|
// (MEM_RESERVE | MEM_COMMIT in one VirtualAlloc: 0 and 7 us). Huge blocks are committed whole at once
|
||||||
|
// anyway, so heap_allocate_block_huge maps them this way: churn_huge missed the huge cache on a quarter
|
||||||
|
// of its steps and spent 13 of its 19.6 us per step releasing such mappings.
|
||||||
|
os_mmap_committed :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void {
|
||||||
|
ptr := os_map(size, alignment, offset, mapped_size, true);
|
||||||
|
#if ENABLE_DECOMMIT if ptr statistics_os_commit(size);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_map :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64, commit: bool) -> *void {
|
||||||
|
os_initialize();
|
||||||
|
#if TEST_HOOKS if test_take_fault(*test_fail_maps) return os_map_failed(size, alignment, offset, mapped_size);
|
||||||
|
|
||||||
|
map_size := size + alignment; // UPSTREAM-BUG U1: wraps for sizes near U64_MAX.
|
||||||
|
// Ok to MEM_COMMIT: according to MSDN, "actual physical pages are not allocated unless/until the
|
||||||
|
// virtual addresses are actually accessed". But with decommit enabled it is better not to commit
|
||||||
|
// immediately and instead commit per page, to avoid saturating the OS commit limit.
|
||||||
|
#if ENABLE_DECOMMIT {
|
||||||
|
do_commit: u32 = ifx commit || global_config.disable_decommit then cast(u32) MEM_COMMIT_W else 0;
|
||||||
|
} else {
|
||||||
|
do_commit: u32 = MEM_COMMIT_W;
|
||||||
|
}
|
||||||
|
base_flags: u32 = MEM_RESERVE_W | do_commit;
|
||||||
|
large_flag: u32 = ifx os_huge_pages then cast(u32) MEM_LARGE_PAGES_W else 0;
|
||||||
|
ptr: *void;
|
||||||
|
if alignment && os_virtualalloc2 {
|
||||||
|
// Map an aligned region directly, so no alignment padding is kept per mapping.
|
||||||
|
reqs: MEM_ADDRESS_REQUIREMENTS_W;
|
||||||
|
reqs.Alignment = alignment;
|
||||||
|
param: MEM_EXTENDED_PARAMETER_W;
|
||||||
|
param.Type = MemExtendedParameterAddressRequirements;
|
||||||
|
param.Pointer = *reqs;
|
||||||
|
map_size = size;
|
||||||
|
ptr = os_virtualalloc2(null, null, map_size, large_flag | base_flags, PAGE_READWRITE_W, *param, 1);
|
||||||
|
if !ptr && large_flag
|
||||||
|
ptr = os_virtualalloc2(null, null, map_size, base_flags, PAGE_READWRITE_W, *param, 1);
|
||||||
|
} else {
|
||||||
|
// Pre Windows 10: over-allocate and offset for alignment below.
|
||||||
|
ptr = VirtualAlloc_w(null, map_size, large_flag | base_flags, PAGE_READWRITE_W);
|
||||||
|
if !ptr && large_flag
|
||||||
|
ptr = VirtualAlloc_w(null, map_size, base_flags, PAGE_READWRITE_W);
|
||||||
|
}
|
||||||
|
if !ptr return os_map_failed(size, alignment, offset, mapped_size);
|
||||||
|
|
||||||
|
if alignment {
|
||||||
|
padding := cast(u64) ptr & (alignment - 1);
|
||||||
|
if padding padding = alignment - padding;
|
||||||
|
rpmalloc_assert(padding <= alignment, "Internal failure in padding");
|
||||||
|
rpmalloc_assert(!(padding % 8), "Internal failure in padding");
|
||||||
|
ptr = pointer_offset(ptr, padding);
|
||||||
|
offset.* = padding;
|
||||||
|
}
|
||||||
|
mapped_size.* = map_size;
|
||||||
|
statistics_os_map(map_size); // 2.0.1: rpmalloc.c:1023-1033
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The failure tail of os_mmap (2.0.1: rpmalloc.c:990-998).
|
||||||
|
// #jai returns null without asserting when there is no map_fail_callback (finding N1).
|
||||||
|
os_map_failed :: (size: u64, alignment: u64, offset: *u64, mapped_size: *u64) -> *void {
|
||||||
|
iface := global_memory_interface;
|
||||||
|
if iface && iface.map_fail_callback && iface.map_fail_callback(size + alignment)
|
||||||
|
return os_mmap(size, alignment, offset, mapped_size);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_commit_on_demand :: inline () -> bool { // 2.0.1: rpmalloc.c:1040
|
||||||
|
#if ENABLE_DECOMMIT return !global_config.disable_decommit;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_mcommit :: (address: *void, size: u64) -> s32 { // 2.0.1: rpmalloc.c:1049
|
||||||
|
#if ENABLE_DECOMMIT {
|
||||||
|
// Gated by callers on the per-span commit_on_demand flag. Spans committed in full at map
|
||||||
|
// time (including large pages, which cannot be partially committed) never reach here.
|
||||||
|
failed := false;
|
||||||
|
#if TEST_HOOKS failed = test_take_fault(*test_fail_commits);
|
||||||
|
if !failed failed = !VirtualAlloc_w(address, size, MEM_COMMIT_W, PAGE_READWRITE_W);
|
||||||
|
if failed {
|
||||||
|
iface := global_memory_interface;
|
||||||
|
if iface && iface.map_fail_callback && iface.map_fail_callback(size)
|
||||||
|
return os_mcommit(address, size);
|
||||||
|
// #jai no assert (finding N1); the caller handles the failure.
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
statistics_os_commit(size); // 2.0.1: rpmalloc.c:1067-1078
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_mdecommit :: (address: *void, size: u64) -> s32 { // 2.0.1: rpmalloc.c:1086
|
||||||
|
#if ENABLE_DECOMMIT {
|
||||||
|
// Unlike os_mcommit, honor the live flag (the Linux fallback can set it mid-run).
|
||||||
|
if global_config.disable_decommit return 1;
|
||||||
|
if !VirtualFree_w(address, size, MEM_DECOMMIT_W) {
|
||||||
|
rpmalloc_assert(false, "Failed to decommit virtual memory block");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
statistics_os_decommit(size); // 2.0.1: rpmalloc.c:1129-1133
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_munmap :: (address: *void, offset: u64, mapped_size: u64) { // 2.0.1: rpmalloc.c:1142
|
||||||
|
address = pointer_offset(address, -cast(s64) offset);
|
||||||
|
#if ENABLE_UNMAP {
|
||||||
|
if !VirtualFree_w(address, 0, MEM_RELEASE_W)
|
||||||
|
rpmalloc_assert(false, "Failed to unmap virtual memory block");
|
||||||
|
statistics_os_unmap(mapped_size); // 2.0.1: rpmalloc.c:1154-1158
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VirtualAlloc2 is resolved at run time from kernelbase.dll (Windows 10 and later).
|
||||||
|
VirtualAlloc2_Proc :: #type (process: *void, base_address: *void, size: u64, allocation_type: u32, page_protection: u32,
|
||||||
|
parameters: *MEM_EXTENDED_PARAMETER_W, parameter_count: u32) -> *void #c_call;
|
||||||
|
os_virtualalloc2: VirtualAlloc2_Proc; // 2.0.1: rpmalloc.c:77
|
||||||
|
|
||||||
|
MEM_ADDRESS_REQUIREMENTS_W :: struct {
|
||||||
|
LowestStartingAddress: *void;
|
||||||
|
HighestEndingAddress: *void;
|
||||||
|
Alignment: u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MEM_EXTENDED_PARAMETER: a 64-bit word (Type:8, Reserved:56) followed by a pointer-sized union.
|
||||||
|
MEM_EXTENDED_PARAMETER_W :: struct {
|
||||||
|
Type: u64;
|
||||||
|
Pointer: *void;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_thread_yield :: () { // 2.0.1: rpmalloc.c:396 (SwitchToThread)
|
||||||
|
SwitchToThread_w();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coarse monotonic time for the huge block cache.
|
||||||
|
os_monotonic_time_ms :: inline () -> u64 { // 2.0.1: rpmalloc.c:1586
|
||||||
|
return GetTickCount64_w();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Thread-local heap storage, thread identity and the thread exit hook.
|
||||||
|
//
|
||||||
|
// #jai upstream keeps the thread heap in a C11 _Thread_local variable and uses FLS only for the
|
||||||
|
// thread exit destructor. This Jai version has no #thread_local, and keeping the heap in the
|
||||||
|
// Context would let copied contexts share a heap between threads (review H3), so the heap pointer
|
||||||
|
// lives in an OS TLS slot (TlsAlloc). An FLS slot (FlsAlloc) carries the exit destructor, as
|
||||||
|
// upstream. Possible performance follow-up: read the TEB slot directly instead of calling TlsGetValue.
|
||||||
|
|
||||||
|
os_tls_create :: (destructor: (value: *void) #c_call) -> bool {
|
||||||
|
tls_index = TlsAlloc_w();
|
||||||
|
if tls_index == TLS_OUT_OF_INDEXES_W return false;
|
||||||
|
fls_index = FlsAlloc_w(destructor); // 2.0.1: rpmalloc.c:2952
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_tls_destroy :: () {
|
||||||
|
FlsFree_w(fls_index); // 2.0.1: rpmalloc.c:3099
|
||||||
|
fls_index = 0;
|
||||||
|
TlsFree_w(tls_index);
|
||||||
|
tls_index = TLS_OUT_OF_INDEXES_W;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #jai fix E10: the calling thread's stack, [low, high).
|
||||||
|
os_thread_stack_range :: () -> low: u64, high: u64 {
|
||||||
|
low, high: u64;
|
||||||
|
GetCurrentThreadStackLimits_w(*low, *high);
|
||||||
|
return low, high;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_tls_get :: inline () -> *void {
|
||||||
|
return TlsGetValue_w(tls_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
os_tls_set :: (value: *void) {
|
||||||
|
TlsSetValue_w(tls_index, value);
|
||||||
|
FlsSetValue_w(fls_index, value); // 2.0.1: rpmalloc.c:803
|
||||||
|
}
|
||||||
|
|
||||||
|
// #jai upstream returns the TEB address (NtCurrentTeb, rpmalloc.c:762). GetCurrentThreadId is
|
||||||
|
// equally unique among live threads, never 0, and needs no inline assembly.
|
||||||
|
// #jai fix U22: true while the process is exiting. ExitProcess stops every other thread first, so a
|
||||||
|
// lock one of them held stays held; the thread-exit destructor must not wait for it then.
|
||||||
|
os_process_exiting :: () -> bool {
|
||||||
|
return RtlDllShutdownInProgress_w() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
os_thread_id :: inline () -> u64 {
|
||||||
|
return GetCurrentThreadId_w();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large page detection (2.0.1: rpmalloc.c:2798-2830).
|
||||||
|
os_detect_huge_pages :: () {
|
||||||
|
token: *void;
|
||||||
|
large_page_minimum := GetLargePageMinimum_w();
|
||||||
|
if large_page_minimum OpenProcessToken_w(GetCurrentProcess_w(), TOKEN_ADJUST_PRIVILEGES_W | TOKEN_QUERY_W, *token);
|
||||||
|
if token {
|
||||||
|
luid: LUID_W;
|
||||||
|
if LookupPrivilegeValueA_w(null, "SeLockMemoryPrivilege".data, *luid) {
|
||||||
|
privileges: TOKEN_PRIVILEGES_W;
|
||||||
|
privileges.PrivilegeCount = 1;
|
||||||
|
privileges.Privileges[0].Luid = luid;
|
||||||
|
privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED_W;
|
||||||
|
if AdjustTokenPrivileges_w(token, 0, *privileges, 0, null, null) {
|
||||||
|
if GetLastError_w() == 0 os_huge_pages = true; // ERROR_SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CloseHandle_w(token);
|
||||||
|
}
|
||||||
|
// Disable huge pages if the large page size is not a power of two that fits the builtin geometry.
|
||||||
|
if os_huge_pages && ((large_page_minimum & (large_page_minimum - 1)) || large_page_minimum > LARGE_PAGE_SIZE)
|
||||||
|
os_huge_pages = false;
|
||||||
|
if os_huge_pages {
|
||||||
|
if large_page_minimum > os_page_size os_page_size = large_page_minimum;
|
||||||
|
if large_page_minimum > os_map_granularity os_map_granularity = large_page_minimum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transparent huge pages are Linux only (2.0.1: rpmalloc.c:2921-2936).
|
||||||
|
os_configure_thp :: () { }
|
||||||
|
|
||||||
|
#scope_file
|
||||||
|
|
||||||
|
kernel32_rpm :: #library,system "kernel32";
|
||||||
|
|
||||||
|
SYSTEM_INFO_W :: struct {
|
||||||
|
wProcessorArchitecture: u16;
|
||||||
|
wReserved: u16;
|
||||||
|
dwPageSize: u32;
|
||||||
|
lpMinimumApplicationAddress: *void;
|
||||||
|
lpMaximumApplicationAddress: *void;
|
||||||
|
dwActiveProcessorMask: u64;
|
||||||
|
dwNumberOfProcessors: u32;
|
||||||
|
dwProcessorType: u32;
|
||||||
|
dwAllocationGranularity: u32;
|
||||||
|
wProcessorLevel: u16;
|
||||||
|
wProcessorRevision: u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
MemExtendedParameterAddressRequirements :: 1;
|
||||||
|
|
||||||
|
MEM_COMMIT_W :: 0x0000_1000;
|
||||||
|
MEM_RESERVE_W :: 0x0000_2000;
|
||||||
|
MEM_DECOMMIT_W :: 0x0000_4000;
|
||||||
|
MEM_RELEASE_W :: 0x0000_8000;
|
||||||
|
MEM_LARGE_PAGES_W :: 0x2000_0000;
|
||||||
|
PAGE_READWRITE_W :: 0x04;
|
||||||
|
|
||||||
|
VirtualAlloc_w :: (address: *void, size: u64, type: u32, protect: u32) -> *void #foreign kernel32_rpm "VirtualAlloc";
|
||||||
|
VirtualFree_w :: (address: *void, size: u64, type: u32) -> s32 #foreign kernel32_rpm "VirtualFree";
|
||||||
|
GetSystemInfo_w :: (info: *SYSTEM_INFO_W) #foreign kernel32_rpm "GetSystemInfo";
|
||||||
|
GetModuleHandleA_w :: (name: *u8) -> *void #foreign kernel32_rpm "GetModuleHandleA";
|
||||||
|
GetProcAddress_w :: (module: *void, name: *u8) -> *void #foreign kernel32_rpm "GetProcAddress";
|
||||||
|
SwitchToThread_w :: () -> s32 #foreign kernel32_rpm "SwitchToThread";
|
||||||
|
GetTickCount64_w :: () -> u64 #foreign kernel32_rpm "GetTickCount64";
|
||||||
|
|
||||||
|
// Thread-local storage and huge page detection.
|
||||||
|
|
||||||
|
TLS_OUT_OF_INDEXES_W :: 0xFFFF_FFFF;
|
||||||
|
tls_index: u32 = TLS_OUT_OF_INDEXES_W;
|
||||||
|
fls_index: u32;
|
||||||
|
|
||||||
|
// The Windows LUID is {DWORD LowPart; LONG HighPart}: 4-byte aligned, so LUID_AND_ATTRIBUTES is 12
|
||||||
|
// bytes and TOKEN_PRIVILEGES.Privileges starts at offset 4. Declaring the LUID as a u64 shifts it
|
||||||
|
// to offset 8, which made large pages silently fail in the 1.4.4 Jai port (review M5).
|
||||||
|
LUID_W :: struct {
|
||||||
|
LowPart: u32;
|
||||||
|
HighPart: s32;
|
||||||
|
}
|
||||||
|
LUID_AND_ATTRIBUTES_W :: struct {
|
||||||
|
Luid: LUID_W;
|
||||||
|
Attributes: u32;
|
||||||
|
}
|
||||||
|
TOKEN_PRIVILEGES_W :: struct {
|
||||||
|
PrivilegeCount: u32;
|
||||||
|
Privileges: [1] LUID_AND_ATTRIBUTES_W;
|
||||||
|
}
|
||||||
|
#assert size_of(TOKEN_PRIVILEGES_W) == 16 "TOKEN_PRIVILEGES layout (review M5)";
|
||||||
|
|
||||||
|
TOKEN_ADJUST_PRIVILEGES_W :: 0x0020;
|
||||||
|
TOKEN_QUERY_W :: 0x0008;
|
||||||
|
SE_PRIVILEGE_ENABLED_W :: 0x0002;
|
||||||
|
|
||||||
|
kernel32_tls :: #library,system "kernel32";
|
||||||
|
GetCurrentThreadStackLimits_w :: (low: *u64, high: *u64) #foreign kernel32_tls "GetCurrentThreadStackLimits";
|
||||||
|
advapi32_tls :: #library,system "advapi32";
|
||||||
|
ntdll_tls :: #library,system "ntdll";
|
||||||
|
RtlDllShutdownInProgress_w :: () -> u8 #foreign ntdll_tls "RtlDllShutdownInProgress";
|
||||||
|
|
||||||
|
TlsAlloc_w :: () -> u32 #foreign kernel32_tls "TlsAlloc";
|
||||||
|
TlsFree_w :: (index: u32) -> s32 #foreign kernel32_tls "TlsFree";
|
||||||
|
TlsGetValue_w :: (index: u32) -> *void #foreign kernel32_tls "TlsGetValue";
|
||||||
|
TlsSetValue_w :: (index: u32, value: *void) -> s32 #foreign kernel32_tls "TlsSetValue";
|
||||||
|
FlsAlloc_w :: (callback: (value: *void) #c_call) -> u32 #foreign kernel32_tls "FlsAlloc";
|
||||||
|
FlsFree_w :: (index: u32) -> s32 #foreign kernel32_tls "FlsFree";
|
||||||
|
FlsSetValue_w :: (index: u32, value: *void) -> s32 #foreign kernel32_tls "FlsSetValue";
|
||||||
|
GetCurrentThreadId_w :: () -> u32 #foreign kernel32_tls "GetCurrentThreadId";
|
||||||
|
GetCurrentProcess_w :: () -> *void #foreign kernel32_tls "GetCurrentProcess";
|
||||||
|
GetLargePageMinimum_w :: () -> u64 #foreign kernel32_tls "GetLargePageMinimum";
|
||||||
|
GetLastError_w :: () -> u32 #foreign kernel32_tls "GetLastError";
|
||||||
|
CloseHandle_w :: (handle: *void) -> s32 #foreign kernel32_tls "CloseHandle";
|
||||||
|
OpenProcessToken_w :: (process: *void, access: u32, token: **void) -> s32 #foreign advapi32_tls "OpenProcessToken";
|
||||||
|
LookupPrivilegeValueA_w :: (system: *u8, name: *u8, luid: *LUID_W) -> s32 #foreign advapi32_tls "LookupPrivilegeValueA";
|
||||||
|
AdjustTokenPrivileges_w :: (token: *void, disable_all: s32, new_state: *TOKEN_PRIVILEGES_W, buffer_length: u32, previous: *void, return_length: *u32) -> s32 #foreign advapi32_tls "AdjustTokenPrivileges";
|
||||||
3490
modules/Default_Allocator/rpmalloc.jai
Normal file
3490
modules/Default_Allocator/rpmalloc.jai
Normal file
File diff suppressed because it is too large
Load Diff
48
modules/Default_Allocator/version.jai
Normal file
48
modules/Default_Allocator/version.jai
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// The version of this port, the same for both flavours (rpmalloc2 and Default_Allocator).
|
||||||
|
//
|
||||||
|
// VERSION is upstream's version followed by this port's revision. Raise VERSION_REVISION with every
|
||||||
|
// change to a file that install.jai installs: an installed copy whose revision is lower is behind,
|
||||||
|
// and install.jai updates it. Raise it here, in VERSION, and in CAPS_VERSION_STRING in module.jai and
|
||||||
|
// Default_Allocator/module.jai; compile-time asserts fail the build until all four agree. To see what
|
||||||
|
// an installed copy is, read this file in it, or the CAPS name its allocator reports
|
||||||
|
// ("rpmalloc 2.0.1-jai.N (Jai port)").
|
||||||
|
|
||||||
|
VERSION :: "2.0.1-jai.16";
|
||||||
|
VERSION_REVISION :: 16;
|
||||||
|
RPMALLOC_VERSION :: "2.0.1"; // The upstream rpmalloc release this is a port of.
|
||||||
|
|
||||||
|
#assert #run version_is(VERSION, RPMALLOC_VERSION, VERSION_REVISION)
|
||||||
|
"VERSION must be RPMALLOC_VERSION \"-jai.\" VERSION_REVISION";
|
||||||
|
|
||||||
|
#scope_module
|
||||||
|
|
||||||
|
// For the compile-time version asserts, here and in both module.jai files. #jai not tprint: they run
|
||||||
|
// as the target's code, and the x64 tprint uses SIMD assembly that a compiler on another CPU cannot
|
||||||
|
// execute (a Linux x64 build on Apple silicon failed). Plain loops run anywhere, and nothing allocates.
|
||||||
|
|
||||||
|
// Is `version` exactly upstream "-jai." revision?
|
||||||
|
version_is :: (version: string, upstream: string, revision: int) -> bool {
|
||||||
|
PREFIX :: "-jai.";
|
||||||
|
if !text_starts(version, 0, upstream) || !text_starts(version, upstream.count, PREFIX) return false;
|
||||||
|
at := upstream.count + PREFIX.count;
|
||||||
|
if at >= version.count return false;
|
||||||
|
n := 0;
|
||||||
|
for i: at..version.count-1 {
|
||||||
|
c := version[i];
|
||||||
|
if c < #char "0" || c > #char "9" return false;
|
||||||
|
n = n * 10 + (c - #char "0");
|
||||||
|
}
|
||||||
|
return n == revision;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is `text` exactly before, middle, after joined?
|
||||||
|
text_is :: (text: string, before: string, middle: string, after: string) -> bool {
|
||||||
|
if text.count != before.count + middle.count + after.count return false;
|
||||||
|
return text_starts(text, 0, before) && text_starts(text, before.count, middle) && text_starts(text, before.count + middle.count, after);
|
||||||
|
}
|
||||||
|
|
||||||
|
text_starts :: (text: string, at: int, part: string) -> bool {
|
||||||
|
if at + part.count > text.count return false;
|
||||||
|
for 0..part.count-1 if text[at + it] != part[it] return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@ -63,6 +63,7 @@ USING_WINDOWS :: OS == .WINDOWS;
|
|||||||
#import "Thread";
|
#import "Thread";
|
||||||
#import "Hash_Table";
|
#import "Hash_Table";
|
||||||
#import "Hash"; // fnv1a_hash, for the label list's hash
|
#import "Hash"; // fnv1a_hash, for the label list's hash
|
||||||
|
Default_Allocator :: #import "Default_Allocator"; // rpmalloc (modules/Default_Allocator), for thread_idle at the workers' waits
|
||||||
Shared_Channel :: #import "Shared_Memory_Channel";
|
Shared_Channel :: #import "Shared_Memory_Channel";
|
||||||
#import "stb_image";
|
#import "stb_image";
|
||||||
#import "stb_image_resize";
|
#import "stb_image_resize";
|
||||||
@ -198,6 +199,10 @@ App :: struct {
|
|||||||
library_folder: string; // -library FOLDER: the .mcap folder the viewer shows, in place of VIEWER_FOLDER
|
library_folder: string; // -library FOLDER: the .mcap folder the viewer shows, in place of VIEWER_FOLDER
|
||||||
scan_and_exit: bool; // -scan: scan the library with the chosen OCR model once it has loaded, then exit (0, or 1 if any capture failed)
|
scan_and_exit: bool; // -scan: scan the library with the chosen OCR model once it has loaded, then exit (0, or 1 if any capture failed)
|
||||||
stats_and_exit: bool; // -stats: log the Statistics window's numbers once the catalog has loaded, then exit
|
stats_and_exit: bool; // -stats: log the Statistics window's numbers once the catalog has loaded, then exit
|
||||||
|
catalog_threads: s64; // -catalog-threads N: workers loading the catalog at once, in place of all of the background group's (for measuring)
|
||||||
|
cold_start: bool;
|
||||||
|
no_catalog_cache: bool; // -no-catalog-cache: read every capture from its file, as without the catalog cache (for measuring)
|
||||||
|
launch_time: Apollo_Time; // monotonic, when main() started: the [startup] log lines count from it // -cold: load the library as on a cold start, its files dropped from the system's file cache first
|
||||||
start_search: string; // -search QUERY: the search box's text at startup
|
start_search: string; // -search QUERY: the search box's text at startup
|
||||||
erase_and_exit: string; // -erase ocr|labels|segments|all: erase that from the whole library once it has loaded, then exit
|
erase_and_exit: string; // -erase ocr|labels|segments|all: erase that from the whole library once it has loaded, then exit
|
||||||
record_and_exit: float64; // -record SECONDS: record the screen under the cursor that long, put it in the library, then exit (0, or 1 if it failed)
|
record_and_exit: float64; // -record SECONDS: record the screen under the cursor that long, put it in the library, then exit (0, or 1 if it failed)
|
||||||
|
|||||||
@ -14,6 +14,24 @@
|
|||||||
|
|
||||||
BACKGROUND_SEARCH_CHUNK :: 256; // captures matched per job
|
BACKGROUND_SEARCH_CHUNK :: 256; // captures matched per job
|
||||||
|
|
||||||
|
// Captures whose catalog entries are wanted, shared by the jobs loading them. Heap; freed by the UI
|
||||||
|
// thread when the last job is back.
|
||||||
|
Catalog_Batch :: struct {
|
||||||
|
items: [] Catalog_Item;
|
||||||
|
cache: string; // the catalog cache's bytes, for items with an offset; heap, freed with the batch
|
||||||
|
next: s64; // the next item a job takes (atomic)
|
||||||
|
jobs_left: s64; // UI thread
|
||||||
|
}
|
||||||
|
|
||||||
|
// A capture whose catalog entry is wanted. path is the capture's, never freed.
|
||||||
|
Catalog_Item :: struct {
|
||||||
|
index: s32;
|
||||||
|
path: string;
|
||||||
|
size: s64 = -1; // its file's, as listed; -1 when not known (catalog_load looks)
|
||||||
|
modified: s64; // nanoseconds
|
||||||
|
offset: s64 = -1; // where its entry starts in the batch's cache; -1: read it from the file
|
||||||
|
}
|
||||||
|
|
||||||
Search_Item :: struct {
|
Search_Item :: struct {
|
||||||
index: s32;
|
index: s32;
|
||||||
entry: *Catalog_Entry; // null while not loaded
|
entry: *Catalog_Entry; // null while not loaded
|
||||||
@ -30,11 +48,14 @@ background: struct {
|
|||||||
stats_generation: s64; // the newest Statistics job
|
stats_generation: s64; // the newest Statistics job
|
||||||
stats_out: bool;
|
stats_out: bool;
|
||||||
retired: [..] *Catalog_Entry; // replaced while jobs were out
|
retired: [..] *Catalog_Entry; // replaced while jobs were out
|
||||||
|
catalog_jobs_out: s64; // UI thread: catalog loads handed out; they read no entries, so jobs_out leaves them out
|
||||||
|
threads: s64;
|
||||||
};
|
};
|
||||||
|
|
||||||
background_startup :: () {
|
background_startup :: () {
|
||||||
background.retired.allocator = context.default_allocator;
|
background.retired.allocator = context.default_allocator;
|
||||||
threads := clamp(get_number_of_processors(.ALL_LOGICAL) - 2, 2, 16);
|
threads := clamp(get_number_of_processors(.ALL_LOGICAL) - 2, 2, 16);
|
||||||
|
background.threads = threads;
|
||||||
// Statistics count words in a table in temporary storage: a few MB on the full library.
|
// 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");
|
init(*background.group, cast(s32) threads, background_proc, temporary_storage_size = 4 * 1024 * 1024, group_name = "background");
|
||||||
background.group.logging = false;
|
background.group.logging = false;
|
||||||
@ -71,8 +92,18 @@ background_frame :: () {
|
|||||||
if !background.started return;
|
if !background.started return;
|
||||||
for work: get_completed_work(*background.group) {
|
for work: get_completed_work(*background.group) {
|
||||||
job := cast(*Background_Job) work;
|
job := cast(*Background_Job) work;
|
||||||
background.jobs_out -= 1;
|
if job.kind == .CATALOG_LOAD || job.kind == .CATALOG_CACHE background.catalog_jobs_out -= 1;
|
||||||
|
else background.jobs_out -= 1;
|
||||||
if job.kind == {
|
if job.kind == {
|
||||||
|
case .CATALOG_CACHE; catalog_cache_indexed(job.cache_index);
|
||||||
|
case .CATALOG_SAVE; catalog_cache_save_done(job.cache_save);
|
||||||
|
case .CATALOG_LOAD;
|
||||||
|
job.catalog.jobs_left -= 1;
|
||||||
|
if !job.catalog.jobs_left {
|
||||||
|
array_free(job.catalog.items,, context.default_allocator);
|
||||||
|
free(job.catalog.cache,, context.default_allocator);
|
||||||
|
free(job.catalog,, context.default_allocator);
|
||||||
|
}
|
||||||
case .SEARCH_EMBED;
|
case .SEARCH_EMBED;
|
||||||
request := job.request;
|
request := job.request;
|
||||||
if request.generation == background.search_generation search_dispatch_chunks(request);
|
if request.generation == background.search_generation search_dispatch_chunks(request);
|
||||||
@ -185,9 +216,47 @@ background_stats_start :: (items: [] Search_Item, model: string) {
|
|||||||
background_add(job);
|
background_add(job);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The catalog entries of these captures, loaded by background_catalog_threads() jobs at once
|
||||||
|
// (app/catalog.jai, catalog_load_job). The entries come back through viewer.catalog_updates.
|
||||||
|
background_catalog_load :: (items: [] Catalog_Item, cache := "") {
|
||||||
|
if !items.count return;
|
||||||
|
batch := New(Catalog_Batch,, context.default_allocator);
|
||||||
|
batch.items = array_copy(items,, context.default_allocator);
|
||||||
|
batch.cache = cache;
|
||||||
|
jobs := min(background_catalog_threads(), items.count);
|
||||||
|
batch.jobs_left = jobs;
|
||||||
|
for 1..jobs {
|
||||||
|
job := New(Background_Job,, context.default_allocator);
|
||||||
|
job.* = .{kind = .CATALOG_LOAD, catalog = batch};
|
||||||
|
background.catalog_jobs_out += 1;
|
||||||
|
add_work(*background.group, job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The startup job reading the catalog cache (app/catalog.jai); catalog_cache_indexed takes it back.
|
||||||
|
background_catalog_cache :: (index: *Catalog_Cache_Index) {
|
||||||
|
job := New(Background_Job,, context.default_allocator);
|
||||||
|
job.* = .{kind = .CATALOG_CACHE, cache_index = index};
|
||||||
|
background.catalog_jobs_out += 1;
|
||||||
|
add_work(*background.group, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A save of the catalog cache. It reads entries, so it counts in jobs_out.
|
||||||
|
background_catalog_save :: (save: *Catalog_Cache_Save) {
|
||||||
|
job := New(Background_Job,, context.default_allocator);
|
||||||
|
job.* = .{kind = .CATALOG_SAVE, cache_save = save};
|
||||||
|
background_add(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every worker of the group by default: with rpmalloc's per-thread heaps, catalog loads scale with
|
||||||
|
// threads (reading 10k files cold: 4 threads 850 ms, 8 450 ms, 16 250 ms). -catalog-threads N, for measuring.
|
||||||
|
background_catalog_threads :: () -> s64 {
|
||||||
|
return clamp(ifx app.catalog_threads > 0 then app.catalog_threads else background.threads, 1, background.threads);
|
||||||
|
}
|
||||||
|
|
||||||
#scope_file
|
#scope_file
|
||||||
|
|
||||||
Background_Kind :: enum u8 { SEARCH_EMBED; SEARCH_CHUNK; SEARCH_MERGE; STATS; }
|
Background_Kind :: enum u8 { SEARCH_EMBED; SEARCH_CHUNK; SEARCH_MERGE; STATS; CATALOG_LOAD; CATALOG_CACHE; CATALOG_SAVE; }
|
||||||
|
|
||||||
Background_Job :: struct {
|
Background_Job :: struct {
|
||||||
kind: Background_Kind;
|
kind: Background_Kind;
|
||||||
@ -200,6 +269,11 @@ Background_Job :: struct {
|
|||||||
matches: [..] Search_Item;
|
matches: [..] Search_Item;
|
||||||
visible: [..] s32;
|
visible: [..] s32;
|
||||||
|
|
||||||
|
// CATALOG_LOAD, CATALOG_CACHE, CATALOG_SAVE
|
||||||
|
catalog: *Catalog_Batch;
|
||||||
|
cache_index: *Catalog_Cache_Index;
|
||||||
|
cache_save: *Catalog_Cache_Save;
|
||||||
|
|
||||||
// STATS
|
// STATS
|
||||||
items: [] Search_Item;
|
items: [] Search_Item;
|
||||||
model: string;
|
model: string;
|
||||||
@ -249,7 +323,14 @@ background_proc :: (group: *Thread_Group, thread: *Thread, work: *void) -> Threa
|
|||||||
case .SEARCH_CHUNK; search_chunk(job);
|
case .SEARCH_CHUNK; search_chunk(job);
|
||||||
case .SEARCH_MERGE; search_merge(job);
|
case .SEARCH_MERGE; search_merge(job);
|
||||||
case .STATS; job.stats = stats_compute(job.items, job.model);
|
case .STATS; job.stats = stats_compute(job.items, job.model);
|
||||||
|
case .CATALOG_LOAD; catalog_load_job(job.catalog);
|
||||||
|
case .CATALOG_CACHE; catalog_cache_index_job(job.cache_index);
|
||||||
|
case .CATALOG_SAVE; catalog_cache_save_job(job.cache_save);
|
||||||
}
|
}
|
||||||
|
// The catalog's jobs are long and come in bursts (startup, a save), after which the workers idle:
|
||||||
|
// what this thread's heap holds free goes back now. Not after each search chunk, which come many
|
||||||
|
// a second.
|
||||||
|
if job.kind == .CATALOG_LOAD || job.kind == .CATALOG_CACHE || job.kind == .CATALOG_SAVE Default_Allocator.thread_idle();
|
||||||
reset_temporary_storage();
|
reset_temporary_storage();
|
||||||
return .CONTINUE;
|
return .CONTINUE;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
// The catalog: what search and sorting need from every capture, loaded once from the front of each
|
// The catalog: what search and sorting need from every capture, loaded once from the front of each
|
||||||
// file (its Capture Text and OCR text, not its images) on the viewer's worker threads.
|
// file (its Capture Text and OCR text, not its images) on the background thread group.
|
||||||
|
//
|
||||||
|
// The catalog cache (CATALOG_CACHE_NAME, in the library folder) keeps every entry with the size and
|
||||||
|
// modified time of the file it was read from, so a start reads one file instead of every capture:
|
||||||
|
// an entry whose file is unchanged comes from the cache, the rest from their files. It is written
|
||||||
|
// once the catalog is complete and has changed, at most every CATALOG_CACHE_SAVE_SECONDS, and at
|
||||||
|
// shutdown.
|
||||||
//
|
//
|
||||||
// Entries are made on the workers and handed over through viewer.catalog_done; only the main thread
|
// Entries are made on the workers and handed over through viewer.catalog_done; only the main thread
|
||||||
// sets or frees viewer.captures[i].catalog, and only it reads them, so no entry is freed while in use.
|
// sets or frees viewer.captures[i].catalog, and only it reads them, so no entry is freed while in use.
|
||||||
@ -25,6 +31,11 @@ Catalog_Entry :: struct {
|
|||||||
segment_count: s32;
|
segment_count: s32;
|
||||||
segments: [..] Catalog_Segment; // largest first
|
segments: [..] Catalog_Segment; // largest first
|
||||||
|
|
||||||
|
// The file it was read from, when it was listed or just before it was read: the catalog cache
|
||||||
|
// gives this entry again only while the file's size and modified time are still these.
|
||||||
|
file_size: s64;
|
||||||
|
file_modified: s64; // nanoseconds
|
||||||
|
|
||||||
// Lowercased copies, for matching without case.
|
// Lowercased copies, for matching without case.
|
||||||
find_title, find_url, find_app, find_notes, find_ocr, find_labels, find_folder: string;
|
find_title, find_url, find_app, find_notes, find_ocr, find_labels, find_folder: string;
|
||||||
find_segments: string; // each class in it once, between newlines: "|person|dog|" with | a newline, for has:
|
find_segments: string; // each class in it once, between newlines: "|person|dog|" with | a newline, for has:
|
||||||
@ -67,8 +78,14 @@ catalog_entry_free :: (entry: *Catalog_Entry) {
|
|||||||
free(entry);
|
free(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The entry for the .mcap at path, or null with why. On any thread.
|
// The entry for the .mcap at path, or null with why. On any thread. size and modified are the file's
|
||||||
catalog_load :: (path: string) -> *Catalog_Entry, error: string {
|
// as listed, when known; otherwise it is looked at first.
|
||||||
|
catalog_load :: (path: string, size: s64 = -1, modified: s64 = 0) -> *Catalog_Entry, error: string {
|
||||||
|
if size < 0 {
|
||||||
|
time, stat_size, stat_ok := file_modtime_and_size(path);
|
||||||
|
if stat_ok { size = stat_size; modified = to_nanoseconds(time); }
|
||||||
|
else { size = 0; modified = 0; } // matches no listing: never given again by the cache
|
||||||
|
}
|
||||||
front: Mcap;
|
front: Mcap;
|
||||||
ok: bool;
|
ok: bool;
|
||||||
error: string;
|
error: string;
|
||||||
@ -80,6 +97,7 @@ catalog_load :: (path: string) -> *Catalog_Entry, error: string {
|
|||||||
|
|
||||||
push_allocator(context.default_allocator);
|
push_allocator(context.default_allocator);
|
||||||
entry := New(Catalog_Entry);
|
entry := New(Catalog_Entry);
|
||||||
|
entry.file_size, entry.file_modified = size, modified;
|
||||||
|
|
||||||
// The metadata and the annotations, from the Capture Text.
|
// The metadata and the annotations, from the Capture Text.
|
||||||
doc: Capture_Document;
|
doc: Capture_Document;
|
||||||
@ -158,6 +176,14 @@ catalog_load :: (path: string) -> *Catalog_Entry, error: string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catalog_finish(entry, path);
|
||||||
|
return entry, "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// What an entry derives from the rest and from where its file is: its folder, and the lowercased
|
||||||
|
// copies search matches. For entries read from files and from the cache alike.
|
||||||
|
catalog_finish :: (entry: *Catalog_Entry, path: string) {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
library := viewer_folder();
|
library := viewer_folder();
|
||||||
if path.count > library.count + 1 && begins_with_nocase(path, library) {
|
if path.count > library.count + 1 && begins_with_nocase(path, library) {
|
||||||
folder := path_strip_filename(advance(path, library.count + 1));
|
folder := path_strip_filename(advance(path, library.count + 1));
|
||||||
@ -182,7 +208,41 @@ catalog_load :: (path: string) -> *Catalog_Entry, error: string {
|
|||||||
append(*all_labels, "\n");
|
append(*all_labels, "\n");
|
||||||
}
|
}
|
||||||
entry.find_labels = to_lower_copy(builder_to_string(*all_labels,, temp));
|
entry.find_labels = to_lower_copy(builder_to_string(*all_labels,, temp));
|
||||||
return entry, "";
|
}
|
||||||
|
|
||||||
|
// On a background worker (app/background.jai): the batch's entries, taken one at a time with the
|
||||||
|
// other jobs on it, each handed to the main thread as it is made. Stops early when the viewer is
|
||||||
|
// shutting down.
|
||||||
|
catalog_load_job :: (batch: *Catalog_Batch) {
|
||||||
|
while true {
|
||||||
|
n := atomic_add(*batch.next, 1);
|
||||||
|
if n >= batch.items.count return;
|
||||||
|
it := batch.items[n];
|
||||||
|
entry: *Catalog_Entry;
|
||||||
|
from_cache := false;
|
||||||
|
if it.offset >= 0 {
|
||||||
|
d := Deserializer.{count = batch.cache.count, data = batch.cache.data, cursor = it.offset};
|
||||||
|
entry = catalog_cache_read_entry(*d);
|
||||||
|
if entry {
|
||||||
|
entry.file_size, entry.file_modified = it.size, it.modified;
|
||||||
|
catalog_finish(entry, it.path);
|
||||||
|
from_cache = true;
|
||||||
|
} else {
|
||||||
|
log_error("[catalog] The cache's entry for % is damaged; reading the file.", it.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !entry {
|
||||||
|
error: string;
|
||||||
|
entry, error = catalog_load(it.path, it.size, it.modified);
|
||||||
|
if !entry log_error("[catalog] % %", it.path, error);
|
||||||
|
}
|
||||||
|
reset_temporary_storage();
|
||||||
|
lock(*viewer.mutex);
|
||||||
|
array_add(*viewer.catalog_updates, .{it.index, entry, from_cache});
|
||||||
|
quit := viewer.quit;
|
||||||
|
unlock(*viewer.mutex);
|
||||||
|
if quit return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
catalog_section :: (entry: *Catalog_Entry, model: string) -> *Mcap_Ocr_Section {
|
catalog_section :: (entry: *Catalog_Entry, model: string) -> *Mcap_Ocr_Section {
|
||||||
@ -191,6 +251,288 @@ catalog_section :: (entry: *Catalog_Entry, model: string) -> *Mcap_Ocr_Section {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- the catalog cache
|
||||||
|
//
|
||||||
|
// "MCAPCATL", u32 CATALOG_CACHE_VERSION, LABELS_MODEL and SEGMENT_MODEL (u32-sized strings), u32 count,
|
||||||
|
// then count records: u32 length of what follows, the path relative to the library, the file's
|
||||||
|
// size and modified time (s64, s64), and the entry's own fields (catalog_cache_add_entry). Its
|
||||||
|
// folder and lowercased copies are not kept: catalog_finish makes them again.
|
||||||
|
|
||||||
|
CATALOG_CACHE_NAME :: ".catalog-cache"; // in the library folder; hidden, so the watcher and listing leave it out
|
||||||
|
CATALOG_CACHE_MAGIC :: "MCAPCATL";
|
||||||
|
CATALOG_CACHE_VERSION :: 1; // raise whenever Catalog_Entry, or what catalog_load takes from a file, changes
|
||||||
|
CATALOG_CACHE_SAVE_SECONDS :: 30.0; // the least time between saves while the app runs
|
||||||
|
|
||||||
|
catalog_cache: struct {
|
||||||
|
dirty: bool; // UI thread: entries changed since the cache was last written
|
||||||
|
saving: bool; // UI thread: a save job is out
|
||||||
|
saved_at: Apollo_Time; // monotonic; zero before the first save
|
||||||
|
};
|
||||||
|
|
||||||
|
catalog_cache_path :: () -> string {
|
||||||
|
return tprint("%/%", viewer_folder(), CATALOG_CACHE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The startup job's: the cache read, and the captures sorted into those it has a current entry for
|
||||||
|
// (items, each with where its entry starts in data) and the rest (misses). All heap.
|
||||||
|
Catalog_Cache_Index :: struct {
|
||||||
|
path: string;
|
||||||
|
captures: [] Catalog_Item; // every capture, with its file's size and modified time as listed
|
||||||
|
data: string; // the cache file; handed to the batch that reads items
|
||||||
|
items: [..] Catalog_Item;
|
||||||
|
misses: [..] Catalog_Item;
|
||||||
|
records, stale: s64; // records in the cache, and those for files changed or gone
|
||||||
|
read_ms, index_ms: float64;
|
||||||
|
problem: string; // why some or all of it was not used (a literal); "" when all was
|
||||||
|
}
|
||||||
|
|
||||||
|
// On a background worker.
|
||||||
|
catalog_cache_index_job :: (job: *Catalog_Cache_Index) {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
job.items.allocator = context.default_allocator;
|
||||||
|
job.misses.allocator = context.default_allocator;
|
||||||
|
start := current_time_monotonic();
|
||||||
|
data, read_ok := read_entire_file(job.path, log_errors = false);
|
||||||
|
job.read_ms = to_float64_seconds(current_time_monotonic() - start) * 1000.0;
|
||||||
|
|
||||||
|
start = current_time_monotonic();
|
||||||
|
matched := NewArray(job.captures.count, bool,, temp);
|
||||||
|
if read_ok {
|
||||||
|
job.data = data;
|
||||||
|
job.problem = catalog_cache_index(job, matched);
|
||||||
|
} else {
|
||||||
|
job.problem = "there is none yet";
|
||||||
|
}
|
||||||
|
for job.captures if !matched[it_index] array_add(*job.misses, it);
|
||||||
|
job.index_ms = to_float64_seconds(current_time_monotonic() - start) * 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The records whose file is unchanged, into job.items. A damaged record ends the walk: those before
|
||||||
|
// it are good, as each says how long it is.
|
||||||
|
catalog_cache_index :: (job: *Catalog_Cache_Index, matched: [] bool) -> problem: string {
|
||||||
|
d := to_deserializer(job.data);
|
||||||
|
if ReadStringView(*d, CATALOG_CACHE_MAGIC.count) != CATALOG_CACHE_MAGIC return "it is not a catalog cache";
|
||||||
|
if Read(*d, u32) != CATALOG_CACHE_VERSION return "another version of mcap wrote it";
|
||||||
|
labels_model := ReadSizedString(*d, u32);
|
||||||
|
segment_model := ReadSizedString(*d, u32);
|
||||||
|
if labels_model != LABELS_MODEL || segment_model != SEGMENT_MODEL return "it was made with other label or segmentation models";
|
||||||
|
count := Read(*d, u32);
|
||||||
|
if !d.ok return "it is damaged";
|
||||||
|
|
||||||
|
folder := viewer_folder();
|
||||||
|
by_path: Table(string, s64);
|
||||||
|
by_path.allocator = temp;
|
||||||
|
for job.captures table_set(*by_path, advance(it.path, folder.count + 1), it_index);
|
||||||
|
for 1..count {
|
||||||
|
length := Read(*d, u32);
|
||||||
|
end := d.cursor + length;
|
||||||
|
relative := ReadSizedString(*d, u32);
|
||||||
|
size := Read(*d, s64);
|
||||||
|
modified := Read(*d, s64);
|
||||||
|
if !d.ok || end > d.count return "it ends early; the rest is read from the files";
|
||||||
|
job.records += 1;
|
||||||
|
found, position := table_find(*by_path, relative);
|
||||||
|
if found && !matched[position] && job.captures[position].size == size && job.captures[position].modified == modified {
|
||||||
|
matched[position] = true;
|
||||||
|
item := job.captures[position];
|
||||||
|
item.offset = d.cursor;
|
||||||
|
array_add(*job.items, item);
|
||||||
|
} else {
|
||||||
|
job.stale += 1;
|
||||||
|
}
|
||||||
|
d.cursor = end;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI thread: the startup job is back. The current entries are read from the cache and the rest from
|
||||||
|
// their files, both on the workers.
|
||||||
|
catalog_cache_indexed :: (job: *Catalog_Cache_Index) {
|
||||||
|
log("[startup] Catalog cache at % ms: % MB read in % ms, sorted in % ms; % of % captures from it, % to read from their files (% of its records stale or gone)%.",
|
||||||
|
FF(startup_ms(), 1), FF(job.data.count / (1024.0 * 1024.0), 1), FF(job.read_ms, 1), FF(job.index_ms, 1),
|
||||||
|
job.items.count, job.captures.count, job.misses.count, job.stale, ifx job.problem then tprint("; %", job.problem) else "");
|
||||||
|
if job.stale || job.problem catalog_cache.dirty = true; // the next save leaves the stale ones out
|
||||||
|
background_catalog_load(job.misses);
|
||||||
|
if job.items.count background_catalog_load(job.items, job.data); // the batch frees data
|
||||||
|
else free(job.data,, context.default_allocator);
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
free(job.path);
|
||||||
|
array_free(job.captures);
|
||||||
|
array_free(job.items);
|
||||||
|
array_free(job.misses);
|
||||||
|
free(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The entry at d's cursor, from the default allocator, without its folder and lowercased copies
|
||||||
|
// (catalog_finish). Null when d runs out.
|
||||||
|
catalog_cache_read_entry :: (d: *Deserializer) -> *Catalog_Entry {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
read_string :: () -> string #expand { return copy_string(ReadSizedString(d, u32)); }
|
||||||
|
entry := New(Catalog_Entry);
|
||||||
|
entry.date = read_string();
|
||||||
|
entry.title = read_string();
|
||||||
|
entry.url = read_string();
|
||||||
|
entry.app = read_string();
|
||||||
|
entry.notes = read_string();
|
||||||
|
entry.label_list = read_string();
|
||||||
|
entry.video_codec = read_string();
|
||||||
|
entry.segment_classes = read_string();
|
||||||
|
entry.media = Read(d, Catalog_Media);
|
||||||
|
entry.duration = Read(d, float64);
|
||||||
|
entry.frame_rate = Read(d, float64);
|
||||||
|
entry.segment_count = Read(d, s32);
|
||||||
|
entry.embedding = array_copy(ReadSizedArray(d, u16, u32));
|
||||||
|
|
||||||
|
entry.labels.allocator = context.default_allocator;
|
||||||
|
array_resize(*entry.labels, Read(d, u32));
|
||||||
|
for * entry.labels { it.score = Read(d, float); it.name = read_string(); }
|
||||||
|
entry.segments.allocator = context.default_allocator;
|
||||||
|
array_resize(*entry.segments, Read(d, u32));
|
||||||
|
for * entry.segments { it.class = read_string(); it.score = Read(d, float); it.area = Read(d, float); }
|
||||||
|
entry.ocr.allocator = context.default_allocator;
|
||||||
|
array_resize(*entry.ocr, Read(d, u32));
|
||||||
|
for * section: entry.ocr {
|
||||||
|
section.model = read_string();
|
||||||
|
section.engine = read_string();
|
||||||
|
section.image = read_string();
|
||||||
|
section.date = read_string();
|
||||||
|
section.image_width = Read(d, s32);
|
||||||
|
section.image_height = Read(d, s32);
|
||||||
|
section.lines.allocator = context.default_allocator;
|
||||||
|
array_resize(*section.lines, Read(d, u32));
|
||||||
|
for * line: section.lines {
|
||||||
|
line.x = Read(d, s32);
|
||||||
|
line.y = Read(d, s32);
|
||||||
|
line.width = Read(d, s32);
|
||||||
|
line.height = Read(d, s32);
|
||||||
|
line.confidence = Read(d, float);
|
||||||
|
line.corners = Read(d, [8] s32);
|
||||||
|
line.text = read_string();
|
||||||
|
if !d.ok break;
|
||||||
|
}
|
||||||
|
if !d.ok break;
|
||||||
|
}
|
||||||
|
if !d.ok { catalog_entry_free(entry); return null; }
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog_cache_add_entry :: (s: *Serializer, entry: *Catalog_Entry) {
|
||||||
|
for string.[entry.date, entry.title, entry.url, entry.app, entry.notes, entry.label_list, entry.video_codec, entry.segment_classes] AddString(s, it, u32);
|
||||||
|
Add(s, entry.media);
|
||||||
|
Add(s, entry.duration);
|
||||||
|
Add(s, entry.frame_rate);
|
||||||
|
Add(s, entry.segment_count);
|
||||||
|
AddSizedArray(s, entry.embedding, u32);
|
||||||
|
Add(s, cast(u32) entry.labels.count);
|
||||||
|
for entry.labels { Add(s, it.score); AddString(s, it.name, u32); }
|
||||||
|
Add(s, cast(u32) entry.segments.count);
|
||||||
|
for entry.segments { AddString(s, it.class, u32); Add(s, it.score); Add(s, it.area); }
|
||||||
|
Add(s, cast(u32) entry.ocr.count);
|
||||||
|
for section: entry.ocr {
|
||||||
|
for string.[section.model, section.engine, section.image, section.date] AddString(s, it, u32);
|
||||||
|
Add(s, section.image_width);
|
||||||
|
Add(s, section.image_height);
|
||||||
|
Add(s, cast(u32) section.lines.count);
|
||||||
|
for line: section.lines {
|
||||||
|
Add(s, line.x); Add(s, line.y); Add(s, line.width); Add(s, line.height);
|
||||||
|
Add(s, line.confidence);
|
||||||
|
Add(s, line.corners);
|
||||||
|
AddString(s, line.text, u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Catalog_Cache_Record :: struct {
|
||||||
|
relative: string; // the capture's path relative to the library: a view into it, never freed
|
||||||
|
entry: *Catalog_Entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cache file's bytes for these records. Default allocator.
|
||||||
|
catalog_cache_serialize :: (records: [] Catalog_Cache_Record) -> Serializer {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
s := new_serializer(records.count * 4096 + 1024);
|
||||||
|
AddArray_NoSize(*s, cast([] u8) CATALOG_CACHE_MAGIC);
|
||||||
|
Add(*s, cast(u32) CATALOG_CACHE_VERSION);
|
||||||
|
AddString(*s, LABELS_MODEL, u32);
|
||||||
|
AddString(*s, SEGMENT_MODEL, u32);
|
||||||
|
Add(*s, cast(u32) records.count);
|
||||||
|
for records {
|
||||||
|
length_at := s.count;
|
||||||
|
Add(*s, cast(u32) 0); // its length, filled in below
|
||||||
|
AddString(*s, it.relative, u32);
|
||||||
|
Add(*s, it.entry.file_size);
|
||||||
|
Add(*s, it.entry.file_modified);
|
||||||
|
catalog_cache_add_entry(*s, it.entry);
|
||||||
|
length := cast(u32) (s.count - length_at - size_of(u32));
|
||||||
|
memcpy(s.data + length_at, *length, size_of(u32));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A save: the entries as they were when it started. Heap. A job holding one counts in
|
||||||
|
// background.jobs_out, so no entry it reads is freed while it runs (background_retire).
|
||||||
|
Catalog_Cache_Save :: struct {
|
||||||
|
path: string;
|
||||||
|
records: [] Catalog_Cache_Record;
|
||||||
|
bytes: s64;
|
||||||
|
ms: float64;
|
||||||
|
ok: bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI thread, each frame: saves once the catalog is complete and has changed.
|
||||||
|
catalog_cache_frame :: () {
|
||||||
|
if !catalog_cache.dirty || catalog_cache.saving || viewer.catalog_loaded < viewer.captures.count return;
|
||||||
|
if catalog_cache.saved_at != .{} && to_float64_seconds(current_time_monotonic() - catalog_cache.saved_at) < CATALOG_CACHE_SAVE_SECONDS return;
|
||||||
|
catalog_cache.dirty = false;
|
||||||
|
catalog_cache.saving = true;
|
||||||
|
background_catalog_save(catalog_cache_snapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog_cache_snapshot :: () -> *Catalog_Cache_Save {
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
save := New(Catalog_Cache_Save);
|
||||||
|
save.path = copy_string(catalog_cache_path());
|
||||||
|
records: [..] Catalog_Cache_Record;
|
||||||
|
array_reserve(*records, viewer.captures.count);
|
||||||
|
folder := viewer_folder();
|
||||||
|
for viewer.captures if !it.removed && it.catalog array_add(*records, .{advance(it.path, folder.count + 1), it.catalog});
|
||||||
|
save.records = records;
|
||||||
|
return save;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On a background worker, or the shutdown thread: written beside itself, then swapped in.
|
||||||
|
catalog_cache_save_job :: (save: *Catalog_Cache_Save) {
|
||||||
|
start := current_time_monotonic();
|
||||||
|
s := catalog_cache_serialize(save.records);
|
||||||
|
defer array_free(s);
|
||||||
|
temp_path := tprint("%.tmp", save.path);
|
||||||
|
save.ok = write_entire_file(temp_path, s.data, s.count) && atomic_file_replace_or_just_rename_if_destination_file_does_not_exist(temp_path, save.path);
|
||||||
|
save.bytes = s.count;
|
||||||
|
save.ms = to_float64_seconds(current_time_monotonic() - start) * 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI thread (or the shutdown thread): a save is done.
|
||||||
|
catalog_cache_save_done :: (save: *Catalog_Cache_Save) {
|
||||||
|
catalog_cache.saving = false;
|
||||||
|
catalog_cache.saved_at = current_time_monotonic();
|
||||||
|
if save.ok log("[catalog] Saved the cache: % entries, % MB in % ms.", save.records.count, FF(save.bytes / (1024.0 * 1024.0), 1), FF(save.ms, 1));
|
||||||
|
else log_error("[catalog] Could not write %; the next start reads the captures from their files.", save.path);
|
||||||
|
push_allocator(context.default_allocator);
|
||||||
|
free(save.path);
|
||||||
|
array_free(save.records);
|
||||||
|
free(save);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On the shutdown thread, once the background group has stopped: the last changes, when the
|
||||||
|
// catalog is complete (an incomplete one would drop what the cache holds of the rest).
|
||||||
|
catalog_cache_save_at_shutdown :: () {
|
||||||
|
if !catalog_cache.dirty && !catalog_cache.saving return;
|
||||||
|
if viewer.catalog_loaded < viewer.captures.count return;
|
||||||
|
save := catalog_cache_snapshot();
|
||||||
|
catalog_cache_save_job(save);
|
||||||
|
catalog_cache_save_done(save);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- search
|
// ---------------------------------------------------------------- search
|
||||||
|
|
||||||
// LIKE is by meaning: the captures whose image is nearest the phrase, ranked by the label model
|
// LIKE is by meaning: the captures whose image is nearest the phrase, ranked by the label model
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
main :: () {
|
main :: () {
|
||||||
using platform; using ImGui;
|
using platform; using ImGui;
|
||||||
// #Startup: Program Startup
|
// #Startup: Program Startup
|
||||||
|
app.launch_time = current_time_monotonic();
|
||||||
app.args = get_command_line_arguments();
|
app.args = get_command_line_arguments();
|
||||||
print_version_and_exit := array_find(app.args, "-version");
|
print_version_and_exit := array_find(app.args, "-version");
|
||||||
|
|
||||||
@ -31,6 +32,9 @@ main :: () {
|
|||||||
app.record_region = .{x, y, x + cast(s32) string_to_int(app.args[it_index + 3]), y + cast(s32) string_to_int(app.args[it_index + 4])};
|
app.record_region = .{x, y, x + cast(s32) string_to_int(app.args[it_index + 3]), y + cast(s32) string_to_int(app.args[it_index + 4])};
|
||||||
}
|
}
|
||||||
app.stats_and_exit = array_find(app.args, "-stats");
|
app.stats_and_exit = array_find(app.args, "-stats");
|
||||||
|
app.cold_start = array_find(app.args, "-cold");
|
||||||
|
app.no_catalog_cache = array_find(app.args, "-no-catalog-cache");
|
||||||
|
for app.args if it == "-catalog-threads" && it_index + 1 < app.args.count app.catalog_threads = string_to_int(app.args[it_index + 1]);
|
||||||
for app.args if it == "-search" && it_index + 1 < app.args.count app.start_search = app.args[it_index + 1];
|
for app.args if it == "-search" && it_index + 1 < app.args.count app.start_search = app.args[it_index + 1];
|
||||||
for app.args if it == "-erase" && it_index + 1 < app.args.count app.erase_and_exit = app.args[it_index + 1];
|
for app.args if it == "-erase" && it_index + 1 < app.args.count app.erase_and_exit = app.args[it_index + 1];
|
||||||
for app.args if it == "-import" && it_index + 2 < app.args.count { app.import_source = app.args[it_index + 1]; app.import_dest = app.args[it_index + 2]; }
|
for app.args if it == "-import" && it_index + 2 < app.args.count { app.import_source = app.args[it_index + 1]; app.import_dest = app.args[it_index + 2]; }
|
||||||
@ -366,6 +370,11 @@ main :: () {
|
|||||||
if app.exit_code exit(app.exit_code); // e.g. a headless test run with failures
|
if app.exit_code exit(app.exit_code); // e.g. a headless test run with failures
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Milliseconds since main() started, for the [startup] log lines.
|
||||||
|
startup_ms :: () -> float64 {
|
||||||
|
return to_float64_seconds(current_time_monotonic() - app.launch_time) * 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns false when the main loop should exit.
|
// Returns false when the main loop should exit.
|
||||||
app_frame :: (in_live_resize := false) -> bool {
|
app_frame :: (in_live_resize := false) -> bool {
|
||||||
using platform; using ImGui;
|
using platform; using ImGui;
|
||||||
|
|||||||
@ -362,7 +362,7 @@ ocr_ui_thread_proc :: (thread: *Thread) -> s64 {
|
|||||||
entry, load_error := catalog_load(item.path);
|
entry, load_error := catalog_load(item.path);
|
||||||
if entry {
|
if entry {
|
||||||
lock(*viewer.mutex);
|
lock(*viewer.mutex);
|
||||||
array_add(*viewer.catalog_updates, .{item.index, entry});
|
array_add(*viewer.catalog_updates, .{item.index, entry, false});
|
||||||
unlock(*viewer.mutex);
|
unlock(*viewer.mutex);
|
||||||
} else {
|
} else {
|
||||||
log_error("[Erase] % was erased, but could not be read back: %", item.path, load_error);
|
log_error("[Erase] % was erased, but could not be read back: %", item.path, load_error);
|
||||||
@ -508,7 +508,7 @@ ocr_read_and_store :: (items: [] Scan_Item, set: *Label_Set) -> [] string {
|
|||||||
entry, load_error := catalog_load(items[n].path);
|
entry, load_error := catalog_load(items[n].path);
|
||||||
if !entry { errors[n] = sprint("stored, but could not be read back: %", load_error); continue; }
|
if !entry { errors[n] = sprint("stored, but could not be read back: %", load_error); continue; }
|
||||||
lock(*viewer.mutex);
|
lock(*viewer.mutex);
|
||||||
array_add(*viewer.catalog_updates, .{items[n].index, entry});
|
array_add(*viewer.catalog_updates, .{items[n].index, entry, false});
|
||||||
unlock(*viewer.mutex);
|
unlock(*viewer.mutex);
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
|
|||||||
@ -271,7 +271,7 @@ segment_read_and_store :: (engine: *Segment_Engine, items: [] Scan_Item) -> [] s
|
|||||||
entry, load_error := catalog_load(item.path);
|
entry, load_error := catalog_load(item.path);
|
||||||
if entry {
|
if entry {
|
||||||
lock(*viewer.mutex);
|
lock(*viewer.mutex);
|
||||||
array_add(*viewer.catalog_updates, .{item.index, entry});
|
array_add(*viewer.catalog_updates, .{item.index, entry, false});
|
||||||
unlock(*viewer.mutex);
|
unlock(*viewer.mutex);
|
||||||
} else {
|
} else {
|
||||||
errors[n] = sprint("was segmented, but could not be read back: %", load_error);
|
errors[n] = sprint("was segmented, but could not be read back: %", load_error);
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
// those in view first and then the rows around them, and are uploaded a few per frame. Textures
|
// those in view first and then the rows around them, and are uploaded a few per frame. Textures
|
||||||
// for previews far out of view are released once there are more than VIEWER_MAX_TEXTURES.
|
// for previews far out of view are released once there are more than VIEWER_MAX_TEXTURES.
|
||||||
//
|
//
|
||||||
// When the workers have no previews to decode they load the catalog (app/catalog.jai), what search
|
// The catalog (app/catalog.jai), what search and sorting need from each capture, loads on the
|
||||||
// and sorting need from each capture. The grid shows viewer.visible: the captures in display order,
|
// background thread group (app/background.jai), so previews keep these workers to themselves. The grid shows viewer.visible: the captures in display order,
|
||||||
// newest first once the catalog is complete, and only those matching the search. Searching runs on
|
// 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.
|
// background workers (app/background.jai), so the UI thread never waits on it.
|
||||||
//
|
//
|
||||||
@ -57,12 +57,15 @@ Viewer_Capture :: struct {
|
|||||||
catalog: *Catalog_Entry; // main thread only; null until loaded
|
catalog: *Catalog_Entry; // main thread only; null until loaded
|
||||||
removed: bool; // main thread only: its file was gone at a refresh; shown nowhere
|
removed: bool; // main thread only: its file was gone at a refresh; shown nowhere
|
||||||
catalog_failed: bool; // main thread only: its catalog entry could not be loaded
|
catalog_failed: bool; // main thread only: its catalog entry could not be loaded
|
||||||
|
file_size: s64 = -1; // its file's, as listed at startup or a refresh; -1 when not known
|
||||||
|
file_modified: s64; // nanoseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// A catalog entry made off the main thread, for the main thread to put in place.
|
// A catalog entry made off the main thread, for the main thread to put in place.
|
||||||
Catalog_Update :: struct {
|
Catalog_Update :: struct {
|
||||||
index: s32;
|
index: s32;
|
||||||
entry: *Catalog_Entry; // null when it could not be loaded
|
entry: *Catalog_Entry; // null when it could not be loaded
|
||||||
|
from_cache: bool; // given by the catalog cache, not read from the file
|
||||||
}
|
}
|
||||||
|
|
||||||
// How the full-size window sizes its image.
|
// How the full-size window sizes its image.
|
||||||
@ -110,7 +113,6 @@ viewer: struct {
|
|||||||
quit: bool;
|
quit: bool;
|
||||||
workers: [VIEWER_WORKER_COUNT] Thread;
|
workers: [VIEWER_WORKER_COUNT] Thread;
|
||||||
workers_started: s64;
|
workers_started: s64;
|
||||||
catalog_next: s64; // the next capture whose catalog entry a worker loads
|
|
||||||
catalog_updates: [..] Catalog_Update; // made, waiting for the main thread
|
catalog_updates: [..] Catalog_Update; // made, waiting for the main thread
|
||||||
|
|
||||||
// Display order and search; main thread only.
|
// Display order and search; main thread only.
|
||||||
@ -118,6 +120,12 @@ viewer: struct {
|
|||||||
visible_dirty := true;
|
visible_dirty := true;
|
||||||
visible_rebuilt_at: Apollo_Time;
|
visible_rebuilt_at: Apollo_Time;
|
||||||
catalog_loaded: s64; // entries put in place, or found unreadable
|
catalog_loaded: s64; // entries put in place, or found unreadable
|
||||||
|
catalog_started: Apollo_Time; // when the workers started on it, for the log line once it is complete
|
||||||
|
catalog_timed: bool; // that line has been logged
|
||||||
|
catalog_from_cache: s64; // entries the catalog cache gave
|
||||||
|
catalog_from_files: s64; // entries read from their files
|
||||||
|
first_preview_logged: bool;
|
||||||
|
first_frame_logged: bool;
|
||||||
removed_count: s64; // captures marked removed
|
removed_count: s64; // captures marked removed
|
||||||
delete_index: s32 = -1; // the capture whose delete is being confirmed
|
delete_index: s32 = -1; // the capture whose delete is being confirmed
|
||||||
delete_without_asking: bool; // until the app restarts
|
delete_without_asking: bool; // until the app restarts
|
||||||
@ -172,13 +180,16 @@ viewer_startup :: () {
|
|||||||
start := current_time_monotonic();
|
start := current_time_monotonic();
|
||||||
for viewer_list_files() {
|
for viewer_list_files() {
|
||||||
capture := array_add(*viewer.captures);
|
capture := array_add(*viewer.captures);
|
||||||
viewer_capture_init(capture, it);
|
viewer_capture_init(capture, it.path);
|
||||||
|
capture.file_size, capture.file_modified = it.size, it.modified;
|
||||||
}
|
}
|
||||||
viewer_update_folders();
|
viewer_update_folders();
|
||||||
// Newest first, roughly: dated names (2026-...) sort after the older {GUID} ones.
|
// Newest first, roughly: dated names (2026-...) sort after the older {GUID} ones.
|
||||||
quick_sort(viewer.captures, (a: Viewer_Capture, b: Viewer_Capture) -> s64 { return compare(b.name, a.name); });
|
quick_sort(viewer.captures, (a: Viewer_Capture, b: Viewer_Capture) -> s64 { return compare(b.name, a.name); });
|
||||||
if !viewer.captures.count viewer.scan_error = sprint("No .% files in %.", MCAP_EXTENSION, viewer_folder());
|
if !viewer.captures.count viewer.scan_error = sprint("No .% files in %.", MCAP_EXTENSION, viewer_folder());
|
||||||
log("[viewer] Found % captures in % in % ms.", viewer.captures.count, viewer_folder(), FF(to_float64_seconds(current_time_monotonic() - start) * 1000.0, 1));
|
log("[startup] Found % captures in % at % ms, listing them in % ms.", viewer.captures.count, viewer_folder(), FF(startup_ms(), 1), FF(to_float64_seconds(current_time_monotonic() - start) * 1000.0, 1));
|
||||||
|
if app.cold_start viewer_evict_file_cache();
|
||||||
|
viewer.catalog_started = current_time_monotonic();
|
||||||
|
|
||||||
for * viewer.workers {
|
for * viewer.workers {
|
||||||
// Reading a front takes 64 KB of temporary storage, and a full image several MB.
|
// Reading a front takes 64 KB of temporary storage, and a full image several MB.
|
||||||
@ -190,6 +201,23 @@ viewer_startup :: () {
|
|||||||
viewer.workers_started += 1;
|
viewer.workers_started += 1;
|
||||||
}
|
}
|
||||||
background_startup();
|
background_startup();
|
||||||
|
{
|
||||||
|
// The catalog: from the cache what it has a current entry for, the rest from the files.
|
||||||
|
items := NewArray(viewer.captures.count, Catalog_Item,, temp);
|
||||||
|
for * items {
|
||||||
|
capture := *viewer.captures[it_index];
|
||||||
|
it.* = .{index = cast(s32) it_index, path = capture.path, size = capture.file_size, modified = capture.file_modified};
|
||||||
|
}
|
||||||
|
if app.no_catalog_cache {
|
||||||
|
log("[startup] -no-catalog-cache: every capture is read from its file.");
|
||||||
|
background_catalog_load(items);
|
||||||
|
} else {
|
||||||
|
index := New(Catalog_Cache_Index,, context.default_allocator);
|
||||||
|
index.path = copy_string(catalog_cache_path(),, context.default_allocator);
|
||||||
|
index.captures = array_copy(items,, context.default_allocator);
|
||||||
|
background_catalog_cache(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
if app.start_search viewer_set_search(app.start_search);
|
if app.start_search viewer_set_search(app.start_search);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,6 +230,7 @@ viewer_shutdown :: () {
|
|||||||
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);
|
||||||
if viewer.open_started thread_is_done(*viewer.open_thread, -1);
|
if viewer.open_started thread_is_done(*viewer.open_thread, -1);
|
||||||
background_shutdown();
|
background_shutdown();
|
||||||
|
catalog_cache_save_at_shutdown(); // the workers have stopped: nothing changes the entries now
|
||||||
viewer_watch_stop();
|
viewer_watch_stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +242,6 @@ viewer_worker :: (thread: *Thread) -> s64 {
|
|||||||
while true {
|
while true {
|
||||||
index: s32 = -1;
|
index: s32 = -1;
|
||||||
full: bool;
|
full: bool;
|
||||||
catalog: bool;
|
|
||||||
request: u64;
|
request: u64;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
||||||
@ -234,15 +262,13 @@ viewer_worker :: (thread: *Thread) -> s64 {
|
|||||||
} else if viewer.queue_next < viewer.queue.count {
|
} else if viewer.queue_next < viewer.queue.count {
|
||||||
index = viewer.queue[viewer.queue_next];
|
index = viewer.queue[viewer.queue_next];
|
||||||
viewer.queue_next += 1;
|
viewer.queue_next += 1;
|
||||||
} else if viewer.catalog_next < viewer.captures.count {
|
|
||||||
// Nothing to show is waiting: load the next catalog entry.
|
|
||||||
catalog = true;
|
|
||||||
index = cast(s32) viewer.catalog_next;
|
|
||||||
viewer.catalog_next += 1;
|
|
||||||
}
|
}
|
||||||
if index >= 0 path = viewer.captures[index].path; // the array grows only under the mutex, and paths are never freed
|
if index >= 0 path = viewer.captures[index].path; // the array grows only under the mutex, and paths are never freed
|
||||||
unlock(*viewer.mutex);
|
unlock(*viewer.mutex);
|
||||||
if index < 0 break; // nothing to do: wait to be woken
|
if index < 0 { // nothing to do: wait to be woken, having given back what this thread's heap holds free
|
||||||
|
Default_Allocator.thread_idle();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if segments {
|
if segments {
|
||||||
overlay := segment_overlay_read(path);
|
overlay := segment_overlay_read(path);
|
||||||
@ -260,16 +286,6 @@ viewer_worker :: (thread: *Thread) -> s64 {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if catalog {
|
|
||||||
entry, error := catalog_load(path);
|
|
||||||
if !entry log_error("[catalog] % %", path, error);
|
|
||||||
reset_temporary_storage();
|
|
||||||
lock(*viewer.mutex);
|
|
||||||
array_add(*viewer.catalog_updates, .{index, entry});
|
|
||||||
unlock(*viewer.mutex);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result := ifx full then viewer_decode_full(path) else viewer_decode_preview(path);
|
result := ifx full then viewer_decode_full(path) else viewer_decode_preview(path);
|
||||||
result.index = index;
|
result.index = index;
|
||||||
|
|
||||||
@ -422,6 +438,10 @@ viewer_frame_gpu_work :: () {
|
|||||||
} else if !capture.texture.handle {
|
} else if !capture.texture.handle {
|
||||||
capture.texture = gpu_texture_create(d.pixels, d.width, d.height);
|
capture.texture = gpu_texture_create(d.pixels, d.width, d.height);
|
||||||
if capture.texture.handle array_add(*viewer.with_texture, d.index);
|
if capture.texture.handle array_add(*viewer.with_texture, d.index);
|
||||||
|
if capture.texture.handle && !viewer.first_preview_logged {
|
||||||
|
viewer.first_preview_logged = true;
|
||||||
|
log("[startup] First preview shown at % ms.", FF(startup_ms(), 1));
|
||||||
|
}
|
||||||
else capture.failed = true;
|
else capture.failed = true;
|
||||||
}
|
}
|
||||||
viewer_free_pixels(*d);
|
viewer_free_pixels(*d);
|
||||||
@ -429,6 +449,7 @@ viewer_frame_gpu_work :: () {
|
|||||||
|
|
||||||
viewer_evict();
|
viewer_evict();
|
||||||
viewer_apply_catalog_updates();
|
viewer_apply_catalog_updates();
|
||||||
|
catalog_cache_frame(); // saves the catalog cache once it is complete and has changed (app/catalog.jai)
|
||||||
viewer_open_frame();
|
viewer_open_frame();
|
||||||
segment_overlay_frame(); // the Capture window's segments: what arrived, and its texture (app/segment_ui.jai)
|
segment_overlay_frame(); // the Capture window's segments: what arrived, and its texture (app/segment_ui.jai)
|
||||||
background_frame(); // takes back search results, and starts a search when visible_dirty (app/background.jai)
|
background_frame(); // takes back search results, and starts a search when visible_dirty (app/background.jai)
|
||||||
@ -446,7 +467,6 @@ viewer_apply_catalog_updates :: () {
|
|||||||
lock(*viewer.mutex);
|
lock(*viewer.mutex);
|
||||||
updates := array_copy(viewer.catalog_updates,, temp);
|
updates := array_copy(viewer.catalog_updates,, temp);
|
||||||
array_reset_keeping_memory(*viewer.catalog_updates);
|
array_reset_keeping_memory(*viewer.catalog_updates);
|
||||||
more_to_load := viewer.catalog_next < viewer.captures.count;
|
|
||||||
unlock(*viewer.mutex);
|
unlock(*viewer.mutex);
|
||||||
|
|
||||||
// Whether the date order can change: a capture's first entry (one a refresh added), one lost, or a
|
// Whether the date order can change: a capture's first entry (one a refresh added), one lost, or a
|
||||||
@ -457,6 +477,8 @@ 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 !capture.catalog capture.catalog_failed = !it.entry;
|
if !capture.catalog capture.catalog_failed = !it.entry;
|
||||||
|
if it.from_cache viewer.catalog_from_cache += 1;
|
||||||
|
else { viewer.catalog_from_files += 1; catalog_cache.dirty = true; }
|
||||||
if !capture.catalog || !it.entry || it.entry.date != capture.catalog.date order_changes = true;
|
if !capture.catalog || !it.entry || it.entry.date != capture.catalog.date order_changes = true;
|
||||||
if it.entry || !capture.catalog {
|
if it.entry || !capture.catalog {
|
||||||
background_retire(capture.catalog); // a search or Stats job may be reading it
|
background_retire(capture.catalog); // a search or Stats job may be reading it
|
||||||
@ -470,9 +492,15 @@ viewer_apply_catalog_updates :: () {
|
|||||||
if !viewer.sorted_by_date && viewer.catalog_loaded >= viewer.captures.count {
|
if !viewer.sorted_by_date && viewer.catalog_loaded >= viewer.captures.count {
|
||||||
viewer.sorted_by_date = true;
|
viewer.sorted_by_date = true;
|
||||||
viewer.visible_dirty = true;
|
viewer.visible_dirty = true;
|
||||||
|
if !viewer.catalog_timed {
|
||||||
|
viewer.catalog_timed = true;
|
||||||
|
failed: s64;
|
||||||
|
for viewer.captures if it.catalog_failed failed += 1;
|
||||||
|
log("[startup] Catalog complete at % ms: % entries in % ms, % from the cache and % read from their files on % threads (% could not be read)%.",
|
||||||
|
FF(startup_ms(), 1), viewer.captures.count, FF(to_float64_seconds(current_time_monotonic() - viewer.catalog_started) * 1000.0, 1),
|
||||||
|
viewer.catalog_from_cache, viewer.catalog_from_files, background_catalog_threads(), failed, ifx app.cold_start then ", from a cold start" else "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// The workers sleep when there is nothing to show; wake them while there is catalog to load.
|
|
||||||
if more_to_load signal(*viewer.wake);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The captures in display order: by date, newest first, once every date is known (by name until
|
// The captures in display order: by date, newest first, once every date is known (by name until
|
||||||
@ -484,7 +512,7 @@ viewer_refresh :: (quiet := false) {
|
|||||||
start := current_time_monotonic();
|
start := current_time_monotonic();
|
||||||
present: Table(string, bool); // the folder's .mcap files now
|
present: Table(string, bool); // the folder's .mcap files now
|
||||||
present.allocator = temp;
|
present.allocator = temp;
|
||||||
for viewer_list_files() table_set(*present, it, true);
|
for viewer_list_files() table_set(*present, it.path, true);
|
||||||
|
|
||||||
removed, added: s64;
|
removed, added: s64;
|
||||||
known: Table(string, bool); // the captures still there
|
known: Table(string, bool); // the captures still there
|
||||||
@ -496,16 +524,19 @@ viewer_refresh :: (quiet := false) {
|
|||||||
removed += 1;
|
removed += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new_items: [..] Catalog_Item;
|
||||||
|
new_items.allocator = temp;
|
||||||
lock(*viewer.mutex);
|
lock(*viewer.mutex);
|
||||||
for _, path: present {
|
for _, path: present {
|
||||||
if table_contains(*known, path) continue;
|
if table_contains(*known, path) continue;
|
||||||
capture := array_add(*viewer.captures);
|
capture := array_add(*viewer.captures);
|
||||||
capture.* = .{};
|
capture.* = .{};
|
||||||
viewer_capture_init(capture, path);
|
viewer_capture_init(capture, path);
|
||||||
|
array_add(*new_items, .{index = cast(s32) (viewer.captures.count - 1), path = capture.path});
|
||||||
added += 1;
|
added += 1;
|
||||||
}
|
}
|
||||||
unlock(*viewer.mutex);
|
unlock(*viewer.mutex);
|
||||||
if added signal(*viewer.wake); // their catalog entries
|
background_catalog_load(new_items);
|
||||||
viewer_update_folders();
|
viewer_update_folders();
|
||||||
|
|
||||||
free(viewer.scan_error);
|
free(viewer.scan_error);
|
||||||
@ -531,7 +562,7 @@ viewer_add_capture :: (path: string) -> s32 {
|
|||||||
viewer_capture_init(capture, path);
|
viewer_capture_init(capture, path);
|
||||||
index = cast(s32) (viewer.captures.count - 1);
|
index = cast(s32) (viewer.captures.count - 1);
|
||||||
unlock(*viewer.mutex);
|
unlock(*viewer.mutex);
|
||||||
signal(*viewer.wake); // its catalog entry
|
background_catalog_load(.[.{index = index, path = capture.path}]);
|
||||||
if viewer.scan_error { free(viewer.scan_error); viewer.scan_error = ""; }
|
if viewer.scan_error { free(viewer.scan_error); viewer.scan_error = ""; }
|
||||||
if !array_find(viewer.folders, capture.folder) viewer_update_folders();
|
if !array_find(viewer.folders, capture.folder) viewer_update_folders();
|
||||||
viewer.sorted_by_date = false; // until its entry is in
|
viewer.sorted_by_date = false; // until its entry is in
|
||||||
@ -539,13 +570,21 @@ viewer_add_capture :: (path: string) -> s32 {
|
|||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every .mcap in the library, in its folders too, but not in hidden ones. Temp.
|
Listed_File :: struct {
|
||||||
viewer_list_files :: () -> [..] string {
|
path: string;
|
||||||
files: [..] string;
|
size: s64;
|
||||||
|
modified: s64; // nanoseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every .mcap in the library, in its folders too, but not in hidden ones, with its size and modified
|
||||||
|
// time (which listing a folder gives at no extra cost on Windows and Unix). Temp.
|
||||||
|
viewer_list_files :: () -> [..] Listed_File {
|
||||||
|
files: [..] Listed_File;
|
||||||
files.allocator = temp;
|
files.allocator = temp;
|
||||||
for file_list(viewer_folder(), recursive = true,, temp) {
|
visit_files(viewer_folder(), true, *files, (info: *File_Visit_Info, files: *[..] Listed_File) {
|
||||||
if viewer_is_library_file(it) array_add(*files, it);
|
if !viewer_is_library_file(info.full_name) return;
|
||||||
}
|
array_add(files, .{copy_string(info.full_name,, temp), info.size, to_nanoseconds(info.modification_time)});
|
||||||
|
});
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -671,6 +710,20 @@ viewer_watch_callback :: (watcher: *Watcher.File_Watcher(void), change: *Watcher
|
|||||||
for viewer.captures if !it.removed && begins_with(it.path, prefix) { viewer_watch.rescan = true; return; }
|
for viewer.captures if !it.removed && begins_with(it.path, prefix) { viewer_watch.rescan = true; return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -cold: every capture's file dropped from the system's file cache, so the catalog and previews are
|
||||||
|
// read from the disk as on the first start after a reboot.
|
||||||
|
viewer_evict_file_cache :: () {
|
||||||
|
start := current_time_monotonic();
|
||||||
|
evicted: s64;
|
||||||
|
for viewer.captures if platform_evict_file_cache(it.path) evicted += 1;
|
||||||
|
platform_evict_file_cache(catalog_cache_path());
|
||||||
|
if !evicted && viewer.captures.count {
|
||||||
|
log("[viewer] -cold: the system's file cache cannot be emptied here; the library is read from whatever it holds.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log("[viewer] -cold: dropped % of % captures from the system's file cache in % ms.", evicted, viewer.captures.count, FF(to_float64_seconds(current_time_monotonic() - start) * 1000.0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
viewer_capture_init :: (capture: *Viewer_Capture, path: string) {
|
viewer_capture_init :: (capture: *Viewer_Capture, path: string) {
|
||||||
push_allocator(context.default_allocator);
|
push_allocator(context.default_allocator);
|
||||||
capture.path = copy_string(path);
|
capture.path = copy_string(path);
|
||||||
@ -704,6 +757,7 @@ viewer_mark_removed :: (index: s32) {
|
|||||||
if capture.removed return;
|
if capture.removed return;
|
||||||
capture.removed = true;
|
capture.removed = true;
|
||||||
viewer.removed_count += 1;
|
viewer.removed_count += 1;
|
||||||
|
catalog_cache.dirty = true; // the next save leaves it out
|
||||||
if capture.texture.handle {
|
if capture.texture.handle {
|
||||||
gpu_texture_release(*capture.texture);
|
gpu_texture_release(*capture.texture);
|
||||||
found, position := array_find(viewer.with_texture, index);
|
found, position := array_find(viewer.with_texture, index);
|
||||||
@ -971,6 +1025,7 @@ viewer_request_full :: (index: s32) {
|
|||||||
// ---------------------------------------------------------------- drawing
|
// ---------------------------------------------------------------- drawing
|
||||||
|
|
||||||
viewer_ui :: () { using ImGui;
|
viewer_ui :: () { using ImGui;
|
||||||
|
if !viewer.first_frame_logged { viewer.first_frame_logged = true; log("[startup] First frame of the grid at % ms.", FF(startup_ms(), 1)); }
|
||||||
viewer_watch_frame(); // files added or deleted outside the app
|
viewer_watch_frame(); // files added or deleted outside the app
|
||||||
viewer_frame_gpu_work();
|
viewer_frame_gpu_work();
|
||||||
|
|
||||||
|
|||||||
@ -133,3 +133,12 @@ platform_set_thread_affinity :: (thread: *Thread, affinity: CPUID_Type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
XDG :: #import "XDG";
|
XDG :: #import "XDG";
|
||||||
|
|
||||||
|
// Drops what the page cache holds of the file, so the next read comes from the disk (-cold). Clean
|
||||||
|
// pages go at once; pages not yet written back stay. False when it cannot be opened.
|
||||||
|
platform_evict_file_cache :: (path: string) -> bool {
|
||||||
|
fd := POSIX.open(temp_c_string(path), POSIX.O_RDONLY | POSIX.O_CLOEXEC);
|
||||||
|
if fd < 0 return false;
|
||||||
|
defer POSIX.close(fd);
|
||||||
|
return POSIX.posix_fadvise(fd, 0, 0, POSIX.POSIX_FADV_DONTNEED) == 0;
|
||||||
|
}
|
||||||
|
|||||||
@ -73,4 +73,10 @@ platform_cjk_font_candidates :: () -> [] Cjk_Font {
|
|||||||
Mach :: #import "macOS"; // task_info
|
Mach :: #import "macOS"; // task_info
|
||||||
|
|
||||||
#import "Objective_C";
|
#import "Objective_C";
|
||||||
#import "Objective_C/AppKit";
|
#import "Objective_C/AppKit";
|
||||||
|
|
||||||
|
// macOS lets no unprivileged program empty its file cache of a file (only `sudo purge`, all of it),
|
||||||
|
// so -cold reads whatever is cached.
|
||||||
|
platform_evict_file_cache :: (path: string) -> bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -646,6 +646,18 @@ platform_map_file_read_only :: (path: string) -> (data: string, ok: bool) {
|
|||||||
return string.{size, cast(*u8) view}, true;
|
return string.{size, cast(*u8) view}, true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drops what the system's file cache holds of the file, so the next read comes from the disk (-cold).
|
||||||
|
// Opening a file unbuffered makes the cache manager flush and purge its pages, when nothing has the
|
||||||
|
// file mapped. False when it cannot be opened.
|
||||||
|
platform_evict_file_cache :: (path: string) -> bool {
|
||||||
|
FILE_FLAG_NO_BUFFERING :: 0x20000000;
|
||||||
|
wide := utf8_to_wide(path,, temp);
|
||||||
|
handle := CreateFileW(wide, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, null, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING);
|
||||||
|
if handle == INVALID_HANDLE_VALUE return false;
|
||||||
|
CloseHandle(handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// "" when the variable is unset or empty. In the current allocator.
|
// "" when the variable is unset or empty. In the current allocator.
|
||||||
windows_environment_variable :: (name: string) -> string {
|
windows_environment_variable :: (name: string) -> string {
|
||||||
name_w := utf8_to_wide(name,, temp);
|
name_w := utf8_to_wide(name,, temp);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// The catalog's pieces on their own (app/catalog.jai, app/viewer.jai): the search query's parsing and
|
// The catalog's pieces on their own (app/catalog.jai, app/viewer.jai): the search query's parsing and
|
||||||
// matching, the RTF stripper on Snagit's annotations, and the grid's order by date.
|
// matching, the RTF stripper on Snagit's annotations, the grid's order by date, and the catalog cache.
|
||||||
|
|
||||||
#scope_file
|
#scope_file
|
||||||
|
|
||||||
@ -136,6 +136,81 @@ order_tests :: () {
|
|||||||
expect(viewer_display_order("2025-01-01 00:00:00", "b", "2025-01-01 00:00:00", "a") < 0, "the same date: by name, newest first");
|
expect(viewer_display_order("2025-01-01 00:00:00", "b", "2025-01-01 00:00:00", "a") < 0, "the same date: by name, newest first");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The catalog cache (app/catalog.jai): an entry written and read back is the same, and only the
|
||||||
|
// records whose file is unchanged are given again.
|
||||||
|
cache_tests :: () {
|
||||||
|
group("cache");
|
||||||
|
folder := viewer_folder();
|
||||||
|
entry := New(Catalog_Entry);
|
||||||
|
entry.date = copy_string("2026-10-07 08:11:15"); entry.title = copy_string("KiCad"); entry.url = copy_string("https://x");
|
||||||
|
entry.app = copy_string("kicad"); entry.notes = copy_string("late\nnote"); entry.label_list = copy_string("f6d0");
|
||||||
|
entry.video_codec = copy_string("h264"); entry.segment_classes = copy_string("ade20k");
|
||||||
|
entry.media = .VIDEO; entry.duration = 12.5; entry.frame_rate = 30; entry.segment_count = 2;
|
||||||
|
entry.embedding = array_copy(u16.[0x3C00, 0x4000, 0x4200]);
|
||||||
|
array_add(*entry.labels, .{0.9, copy_string("Schematic")}, .{0.4, copy_string("PCB layout")});
|
||||||
|
array_add(*entry.segments, .{copy_string("person"), 0.8, 0.25});
|
||||||
|
section := array_add(*entry.ocr);
|
||||||
|
section.* = .{model = copy_string("ppocr"), engine = copy_string("ort"), image = copy_string("rendered"), image_width = 640, image_height = 480, date = copy_string("2026-10-08T21:04:11Z")};
|
||||||
|
array_add(*section.lines, .{1, 2, 3, 4, 0.75, copy_string("0.1uF : Capacitor_SMD"), .[1, 2, 3, 4, 5, 6, 7, 8]});
|
||||||
|
entry.file_size, entry.file_modified = 1234, 5678;
|
||||||
|
|
||||||
|
records := Catalog_Cache_Record.[.{"A/one.mcap", entry}, .{"A/two.mcap", entry}];
|
||||||
|
bytes := catalog_cache_serialize(records);
|
||||||
|
defer array_free(bytes);
|
||||||
|
|
||||||
|
// one.mcap unchanged; two.mcap changed since; three.mcap not in the cache.
|
||||||
|
job: Catalog_Cache_Index;
|
||||||
|
job.data = cast(string) bytes;
|
||||||
|
captures := Catalog_Item.[
|
||||||
|
.{index = 0, path = tprint("%/A/one.mcap", folder), size = 1234, modified = 5678},
|
||||||
|
.{index = 1, path = tprint("%/A/two.mcap", folder), size = 1234, modified = 9999},
|
||||||
|
.{index = 2, path = tprint("%/A/three.mcap", folder), size = 1, modified = 1}];
|
||||||
|
job.captures = captures;
|
||||||
|
matched: [3] bool;
|
||||||
|
problem := catalog_cache_index(*job, matched);
|
||||||
|
expect(problem == "", "a cache it wrote is read whole: %", problem);
|
||||||
|
expect(job.records == 2 && job.items.count == 1 && job.items[0].index == 0 && job.stale == 1, "only the unchanged file's record is given: % records, % items, % stale", job.records, job.items.count, job.stale);
|
||||||
|
expect(matched[0] && !matched[1] && !matched[2], "the others are read from their files");
|
||||||
|
|
||||||
|
if job.items.count {
|
||||||
|
d := Deserializer.{count = bytes.count, data = bytes.data, cursor = job.items[0].offset};
|
||||||
|
back := catalog_cache_read_entry(*d);
|
||||||
|
expect(back != null, "the entry reads back");
|
||||||
|
if back {
|
||||||
|
catalog_finish(back, captures[0].path);
|
||||||
|
expect(back.date == entry.date && back.title == entry.title && back.url == entry.url && back.app == entry.app && back.notes == entry.notes, "its texts");
|
||||||
|
expect(back.media == .VIDEO && back.duration == 12.5 && back.frame_rate == 30 && back.video_codec == "h264", "its media");
|
||||||
|
expect(back.label_list == "f6d0" && back.labels.count == 2 && back.labels[1].name == "PCB layout" && back.labels[0].score == 0.9, "its labels");
|
||||||
|
expect(back.embedding.count == 3 && back.embedding[2] == 0x4200, "its image vector");
|
||||||
|
expect(back.segment_count == 2 && back.segments.count == 1 && back.segments[0].class == "person" && back.segments[0].area == 0.25, "its segments");
|
||||||
|
ok := back.ocr.count == 1 && back.ocr[0].model == "ppocr" && back.ocr[0].image_height == 480 && back.ocr[0].lines.count == 1;
|
||||||
|
if ok { line := back.ocr[0].lines[0]; ok = line.text == "0.1uF : Capacitor_SMD" && line.height == 4 && line.confidence == 0.75 && line.corners[7] == 8; }
|
||||||
|
expect(ok, "its OCR lines");
|
||||||
|
expect(back.find_ocr == "0.1uf : capacitor_smd\n" && back.find_folder == "a" && back.find_labels == "schematic\n", "catalog_finish makes its folder and lowercased copies: %, %, %", back.find_ocr, back.find_folder, back.find_labels);
|
||||||
|
catalog_entry_free(back);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
array_free(job.items);
|
||||||
|
array_free(job.misses);
|
||||||
|
|
||||||
|
// A cache from another version is not used at all.
|
||||||
|
(cast(*u32) (bytes.data + CATALOG_CACHE_MAGIC.count)).* = CATALOG_CACHE_VERSION + 1;
|
||||||
|
other: Catalog_Cache_Index;
|
||||||
|
other.data = cast(string) bytes;
|
||||||
|
other.captures = captures;
|
||||||
|
matched = .[false, false, false];
|
||||||
|
expect(catalog_cache_index(*other, matched) != "" && !other.items.count, "another version's cache gives nothing");
|
||||||
|
// Truncated: the records before the damage are still good.
|
||||||
|
(cast(*u32) (bytes.data + CATALOG_CACHE_MAGIC.count)).* = CATALOG_CACHE_VERSION;
|
||||||
|
short: Catalog_Cache_Index;
|
||||||
|
short.data = string.{bytes.count - 10, bytes.data};
|
||||||
|
short.captures = captures;
|
||||||
|
matched = .[false, false, false];
|
||||||
|
expect(catalog_cache_index(*short, matched) != "" && short.items.count == 1, "a cache cut short gives the records before the cut");
|
||||||
|
array_free(other.items); array_free(short.items);
|
||||||
|
catalog_entry_free(entry);
|
||||||
|
}
|
||||||
|
|
||||||
#scope_export
|
#scope_export
|
||||||
|
|
||||||
run_catalog_tests :: (builder: *String_Builder = null) -> bool {
|
run_catalog_tests :: (builder: *String_Builder = null) -> bool {
|
||||||
@ -146,5 +221,6 @@ run_catalog_tests :: (builder: *String_Builder = null) -> bool {
|
|||||||
match_tests();
|
match_tests();
|
||||||
rtf_tests();
|
rtf_tests();
|
||||||
order_tests();
|
order_tests();
|
||||||
|
cache_tests();
|
||||||
return report();
|
return report();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user