Performance Diagnostics and Optimization
Categories:
indexly perf measures the current local search database against its own
bounded history. It is an evidence tool, not a hardware benchmark and not a
claim that a large or diverse index is unhealthy.
Performance values are derived from this local Indexly installation and its current state. They are not a hardware benchmark, guarantee, or direct comparison with another machine. Theoretical values describe modelled potential only; actual results vary with data shape, storage, optional tools, operating-system activity, and the command being run.
Choose a Command
| Goal | Command | Search database effect | Local record effect |
|---|---|---|---|
| Collect a bounded live report | indexly perf --show |
Opens non-WAL SQLite read-only; refuses WAL mode before opening SQLite | Atomically refreshes the performance record |
| Read the latest validated report | indexly perf --read |
Does not open SQLite | Does not write |
| Build an optimization plan | indexly perf --opti |
Does not change SQLite | Does not write |
| Apply an eligible action | indexly perf --opti --action <name> --apply --backup-dir PATH |
Runs one guarded SQLite action after confirmation | Appends a bounded numeric audit and refreshes the report |
Use a different search database when you are intentionally diagnosing a copy or fixture:
indexly perf --show --db path/to/fts_index.db
--db is used by --show and an applied action. The --read and plan-only
--opti paths do not resolve or inspect it because they do not open SQLite.
Add --json to --show, --read, or the non-mutating --opti plan when a
script needs structured output:
indexly perf --show --json
indexly perf --read --json
indexly perf --opti --json
Each successful JSON command writes one document:
| Mode | Schema | Mode-specific fields |
|---|---|---|
--show |
indexly.performance-report/v1 |
mode: "show", record_source: "refreshed", recovered_prior, and record |
--read |
indexly.performance-report/v1 |
mode: "read", record_source, recovered_from_previous, and record |
Plan-only --opti |
indexly.performance-plan/v1 |
mode: "opti", mutating: false, status, action-specific recommendations, enabled_actions, and apply_eligibility |
| Successful applied action | indexly.performance-action/v1 |
mutating: true, mutation_applied, retained backup filename, audit state, postcheck comparison, numeric action_outcome, and refreshed public record |
| Rolled-back action with retained backup | indexly.performance-action/v1 |
mutation_applied: false, rolled_back: true, backup_retained: true, backup filename, and structured error |
| Applied mutation with failed audit or postcheck | indexly.performance-action/v1 |
mutation_applied: true, retained backup filename, audit state, failed postcheck when applicable, and numeric action_outcome |
| Backup failure with incomplete cleanup | indexly.performance-error/v1 |
mutation_applied: false, backup_verified: false, cleanup_incomplete: true, unverified candidate filename, and structured error |
The public record includes the status, sessions, baselines, numeric metrics,
database correlation digest, schema fingerprint, and timestamps. It omits the
private identity salt. Usage, evidence, record, and preflight failures use
indexly.performance-error/v1. Every JSON invocation prints exactly one
document.
Exit status distinguishes an ordinary failure from a committed action whose follow-up processing failed:
| Exit status | Meaning |
|---|---|
0 |
The requested report, plan, read, or applied action completed successfully |
2 |
No database mutation committed: usage, evidence, record, preflight, backup, or execution failed; an action rolled back after creating a verified backup also exits 2, as do failed follow-up for a no_op outcome and incomplete cleanup of an unverified backup candidate |
3 |
A database mutation was applied, but the numeric audit could not be persisted or the post-action report/comparison failed |
Treat 3 as partial success: preserve the reported backup and inspect the
database and local performance record before retrying. A blind retry could
repeat an action that already committed.
Command side effects
--show opens the selected SQLite database in read-only, query-only mode. It
may read bounded indexing-log samples and file sizes, then changes only the
local performance-record files. It does not change the database, its
WAL/journal sidecars, schema, search cache, Indexly logs, or indexed source
files.
SQLite can create or update a shared-memory sidecar even for a read-only
connection to a WAL-mode database. Indexly therefore detects WAL mode from the
database header and refuses the live probe before opening SQLite. Diagnose a
verified backup, or use an authorized SQLite workflow to checkpoint the
database and leave WAL mode before retrying. perf never changes journal mode
itself.
--read validates the saved record without opening SQLite. It creates no
runtime directory, temporary file, lock, cache, database, or replacement
record.
--opti without --apply is plan-only. The abbreviated command name never
implies permission to modify a database. An action requires --action,
--apply, and --backup-dir together. Without --yes, an interactive
terminal must confirm by typing the exact action name. JSON-mode apply is
non-interactive and therefore requires --yes.
Platform support for applied actions
--show, --read, and plan-only --opti work on supported platforms. Applied
planner-optimize and fts-merge actions additionally require a POSIX runtime
that can durably synchronize the backup directory. Windows cannot provide the
required directory fsync guarantee, so Indexly intentionally fails closed
before changing SQLite. Run an applied action on a supported POSIX host; do not
weaken the backup requirement to work around this limitation.
Read the Report
Every reported metric is labelled by where it came from:
- Observed: directly measured from a bounded local file, SQLite query, or Indexly indexing-log summary.
- Indexly-derived: calculated from observed values or comparable local samples by an Indexly formula.
- Theoretical: a descriptive model of possible capacity or growth. It is not a prediction or expected space saving.
Collected report values
| Value | Label | Unit and source | Interpretation |
|---|---|---|---|
| Main database size | Observed | Bytes from the database file | Current on-disk size of the main SQLite file |
| Sidecar size | Observed | Bytes from present WAL, SHM, or journal files | Current auxiliary SQLite storage |
| Page count and page size | Observed | Pages and bytes per page from SQLite | Inputs to allocated-size calculations |
| Database change counter | Observed | Unsigned counter from bytes 24–27 of the official SQLite database header | Privacy-safe binding that detects a committed database-file change since the report |
| Freelist count | Observed | Pages from SQLite | Reusable pages currently managed inside SQLite |
| Journal mode | Observed context | SQLite journal-mode value | Context required before comparing sessions |
| Document count | Observed | Documents from a budgeted COUNT(*) |
Corpus context, not a health score |
| FTS definition fingerprint | Observed context | Canonical FTS inspection result | Detects whether samples describe the same search structure |
| FTS action readiness | Observed | Boolean result from canonical Indexly FTS5 inspection | 1 only when inspection state is match; otherwise maintenance routes to Doctor |
| Planner refresh candidates | Observed or Indexly-derived | Action count from SQLite’s debug-only optimize probe, or a bounded relational-index fallback | Action-specific evidence; not a generic performance grade |
| Vocabulary readiness latency | Indexly-derived | Milliseconds from bounded timed probes | Whether the FTS vocabulary helper responds |
| FTS readiness latency | Indexly-derived | Milliseconds from bounded MATCH ... LIMIT 1 probes |
Whether a content-free readiness probe completes |
| Recent indexing throughput | Indexly-derived | Documents per second from bounded INDEX_SUMMARY records |
Recent Indexly indexing evidence |
| Cache size | Observed | Bytes from the cache file | Size only; performance diagnostics do not read cached search content |
The internal FTS probe term is never printed, saved, or returned. The probe does not return document content. A missing vocabulary table, unavailable internal term, or expired probe budget is reported as readiness evidence, not as a request to inspect source content.
The database change counter is read directly without opening SQLite and contains no path or content. It is the big-endian value at header offset 24 defined by SQLite’s database-header format.
Derived and theoretical formulas
| Value | Label | Formula |
|---|---|---|
| Allocated database bytes | Indexly-derived | page_count × page_size |
| Freelist ratio | Indexly-derived | 100 × freelist_count / max(page_count, 1) |
| Bytes per document | Indexly-derived | allocated_database_bytes / max(document_count, 1) |
| Probe p50 | Indexly-derived | Median of nine timed runs after two warm-ups |
| Probe p95 | Indexly-derived | Nearest rank at ceil(0.95 × sample_count) |
| Recent indexing throughput | Indexly-derived | Median of indexed_count / duration_seconds |
| Potential free-page bytes | Theoretical | freelist_count × page_size |
| Page-limit utilization | Theoretical | allocated_bytes / (max_page_count × page_size) |
Formula results depend on the validity and availability of their inputs. A missing or timed-out input produces an unavailable or inconclusive value; it is not silently treated as zero.
For comparable records at least one day apart, Indexly emits the theoretical
growth rate (allocated_now - allocated_prior) / elapsed_days in bytes per
day. It omits the value when the records are not comparable or are less than
one day apart.
Free pages are reusable by SQLite and are not guaranteed filesystem space savings. Page-limit utilization is a file-format capacity ratio, not a performance grade.
Local Baselines
Indexly compares a sample only with records for the same:
- non-reversible database identity
- schema and FTS fingerprint
- SQLite and Indexly version
- journal mode
- page size
- database-size bucket
The size buckets are 0–128 MiB, 128–512 MiB, 512 MiB–2 GiB,
2–10 GiB, and >10 GiB. A changed identity, fingerprint, version, journal
mode, page size, or size bucket starts a new baseline instead of forcing an
invalid comparison.
A baseline reset does not erase completed action audits when the database identity is unchanged. For example, crossing a size bucket starts new observation history while retaining up to 30 existing action outcomes. A different database identity starts a new record and clears those outcomes.
Classification needs a current observation plus three earlier successful,
comparable --show sessions. Until the fourth comparable observation, the
evidence state is collecting_baseline. The record retains at most 30 sessions,
and calculations use the latest 15 valid, comparable sessions.
Each classified timed metric also needs three measured prior values; sparse or
budget-exhausted metric history remains inconclusive.
Collect representative observations
The four-observation requirement is a minimum classification gate, not an
instruction to run indexly perf --show four times immediately. Indexly does
not enforce a time interval between comparable sessions, so back-to-back runs
can technically satisfy the count. They are nevertheless weak baseline
evidence because they repeatedly sample nearly the same operating moment.
Successive runs are strongly correlated: the operating-system and SQLite caches are likely to remain warm, the indexed corpus and workload have barely changed, and CPU, storage, and background activity are similar. This can bias the baseline in either direction:
- repeated warm-cache timings can understate normal latency and produce an artificially narrow boundary, making later ordinary variation look degraded;
- repeated runs during one transient busy period can establish an unrepresentatively slow baseline and hide later degradation;
- either pattern misses the variation that occurs across normal indexing, searching, storage, and host activity.
Use observations from distinct, representative operating periods:
- Run
indexly perf --showonce after the normal corpus is available. - Collect each subsequent observation after another typical period of indexing and searching, rather than immediately after the preceding run.
- As a practical default for a regularly used installation, collect one observation per normal operating day until four comparable observations exist. For weekly or batch-oriented installations, collect after each representative workload cycle instead.
- Prefer a repeatable point in the workflow, such as after the normal indexing batch has completed and the installation has returned to its usual state. Avoid one-off backups, upgrades, antivirus scans, or unusual host load unless those conditions are part of normal operation.
There is no mandatory one-day interval for baseline classification. The one-day requirement applies only to the separate database-growth calculation. The goal is representative separation, not an arbitrary delay or deliberately manufactured variation.
planner-optimize can use current action-specific planner evidence without a
complete timed baseline. fts-merge is intentionally stricter: it requires a
local FTS-readiness baseline, comparable observations, sustained degradation,
and advancing search-index generations. Do not collect immediate duplicate
sessions merely to make an action appear eligible.
For a timed metric, Indexly calculates:
m = median(valid sessions)
MAD = median(abs(sample - m))
robust_sigma = 1.4826 × MAD
boundary = max(1.25 × baseline_p95, m + 3 × robust_sigma)
For lower-is-better measures, a result above the boundary is degraded.
Throughput uses the inverse direction. Indexly requires two successive degraded
sessions before reporting Elevated.
Constrained is reserved for stronger evidence: p95 above twice the baseline,
a result above m + 6 × robust_sigma, a critical probe above two seconds, or a
bounded snapshot above ten seconds. These are Indexly policy defaults, not
global SQLite or FTS5 thresholds. Sites with established service objectives
should keep using those objectives alongside the numeric report.
Status Meanings
| Status | Meaning | Next step |
|---|---|---|
Nominal |
A current report contains enough evidence and shows no material pressure | Continue normal monitoring |
Elevated |
Repeated baseline-relative degradation was observed | Review indexly perf --show and the plan from --opti |
Constrained |
A bounded probe or sustained deviation is materially slow | Investigate storage, workload, and integrity before maintenance |
States such as not_assessed, collecting_baseline, baseline_stale,
record_unavailable, and inconclusive are evidence states, not performance
grades. Do not interpret them as healthy or unhealthy.
Doctor treats a validated record older than 30 days as baseline_stale.
Doctor’s boundary
Plain indexly doctor remains read-only. It reads only the conservative status
published by the performance module. Doctor does not collect performance
metrics, run timing queries, calculate baselines, invoke perf --opti, or infer
database corruption from a slow measurement.
Doctor shows only a grade or an ungraded evidence state and points to the full report:
Performance: Elevated — current local evidence indicates sustained FTS-read pressure.
Run: indexly perf --show
Use indexly doctor --full-integrity when evidence suggests an integrity or
schema problem. Performance status does not authorize repair.
Performance Record and Privacy
The versioned local record is stored under the configured Indexly runtime directory:
<INDEXLY_HOME>/perf/performance-v1.json
<INDEXLY_HOME>/perf/performance-v1.previous.json
If INDEXLY_HOME is not set, Indexly uses its platform-specific runtime
directory. See Configuration.
The record includes bounded numeric samples, calculation context, timestamps, a schema version, a checksum over canonical content, and up to 30 numeric action outcomes. Its database identity is a salted, local, non-reversible digest.
The record does not contain paths, source roots, filenames, indexed content, metadata JSON, query terms, usernames, hostnames, raw logs, or network telemetry. Indexly does not upload the record.
Atomic update and recovery
During --show, Indexly validates the existing record, writes and synchronizes
a validated previous copy, writes and synchronizes a same-directory temporary
replacement, and atomically replaces the primary record.
--read validates the primary record first and then the previous record. If
the primary is incomplete or invalid but the previous record is valid, Indexly
reports recovery from the previous file without promoting, replacing, or
rewriting it. If record files exist but neither copy validates, --show
refuses to overwrite them. Preserve or relocate both invalid files for incident
review, then run indexly perf --show to start a fresh record.
Plan Maintenance Conservatively
Run the non-mutating planner first:
indexly perf --opti
The plan evaluates each supported action against its own evidence. It does not
enable maintenance from a generic status, database size, page count, file
size, theoretical value, or Nominal/Elevated/Constrained grade alone.
With no validated record, both actions remain at collect_evidence.
Plan-only --opti deliberately does not open the database. A recommendation
therefore describes recorded evidence; final apply eligibility is verified
separately against the selected live database.
Action-specific evidence
Both actions first require canonical Indexly FTS5 schema readiness:
- If the
fts_schema_action_readymetric is absent from a legacy record, the disposition iscollect_evidence; run a freshindexly perf --show. - If readiness exists but is unmeasured, budget-unavailable, malformed as
evidence, or otherwise unavailable, the disposition is
unavailable. No schema damage is inferred and no action is eligible. - If readiness is measured as
0, the disposition isrepair_required. This represents an actually inspected noncanonical state such as a missing, drifted, external-content, malformed, unsupported, or uninspectable FTS definition. Review Indexly Doctor. - Only measured value
1, canonical inspection statematch, permits the action-specific evidence checks below.
| Action | Required recorded evidence |
|---|---|
planner-optimize |
Python’s SQLite runtime is 3.46 or newer and the latest bounded planner probe reports one or more candidates, either from side-effect-free PRAGMA main.optimize(0x10013) output or the read-only fallback described below |
fts-merge |
A local fts_readiness_p95_ms baseline plus three latest comparable observations with strictly advancing search-index generations; the latest two p95 observations must both exceed the baseline degradation boundary |
The planner probe stores only the number of proposed operations. It discards
the returned SQL because schema object names are outside the performance
record’s privacy contract. Some SQLite builds return SQLITE_READONLY when
the debug pragma prepares planner-stat state on an FTS database. Indexly keeps
the connection read-only and falls back to an explicitly Indexly-derived count
of known relational tables (file_tags, file_metadata, and indexly_state)
whose indexes lack sqlite_stat1 coverage. The fallback does not inspect FTS5
shadow tables. Other database errors or an exhausted probe budget remain
unavailable evidence and do not authorize an action. See SQLite’s
PRAGMA optimize reference
for the upstream bit-mask semantics.
fts-merge never estimates eligibility from unsupported FTS shadow-table
segment counts. When eligible, it runs exactly one positive, fixed-size
500-page FTS5 merge command. It is not the FTS5 full optimize command. See
SQLite’s FTS5 merge command.
Apply an eligible action
First collect a current report and inspect the plan:
indexly perf --show
indexly perf --opti
indexly perf --opti --action planner-optimize --apply \
--backup-dir /path/to/indexly-backups
For reviewed non-interactive automation, add --yes. In JSON mode, --yes is
mandatory:
indexly perf --opti --action fts-merge --apply \
--backup-dir /path/to/indexly-backups --yes --json
Apply fails closed unless all of these checks pass:
- The primary performance record validates, was not recovered from the previous copy, and its latest observation is no more than 24 hours old.
- The selected database exactly matches the recorded non-reversible identity and structured schema/FTS fingerprint.
- Indexly version, SQLite version, journal mode, page size, page count, main
database file bytes, observed database change counter, document count,
coarse size bucket, and measured
search_index_generationstill match the report. - Neither the report, SQLite header, nor a present
-walsidecar indicates WAL. Indexly rechecks journal mode after opening and never checkpoints WAL or changes journal mode. - The explicit backup directory already exists, is a directory, is not a symbolic link, and differs from the live database directory. The database and backup filesystems have sufficient free space for conservative action workspace, the snapshot, and margin.
BEGIN IMMEDIATEobtains the sole writer reservation immediately. A busy database is refused before mutation.- The source passes SQLite’s scalable
PRAGMA quick_check; Indexly creates a generic, SQLite-readable backup with SQLite’s backup API, verifiesquick_checkand source/locked/backup invariants, and secures the completed snapshot before running the action. Forfts-merge, the backup must also pass FTS5’sintegrity-checkcommand. Indexly exposes the completed backup filename only after the backup file is synchronized, atomically renamed, and the backup directory itself is successfully synchronized. Directory synchronization failure aborts before the action runs and triggers cleanup of backup candidates. - The terminal confirmation exactly matches the action name, or
--yeswas supplied explicitly.
The writer reservation is retained across backup and action execution. For
background on the guarantees involved, see SQLite’s
backup API and
BEGIN IMMEDIATE transaction
documentation.
planner-optimize applies the bounded matching mask
PRAGMA main.optimize(0x10012) and requires SQLite 3.46 or newer.
fts-merge submits one 500-page merge. After either action, Indexly verifies
quick_check and unchanged logical invariants on the live database before
commit. An FTS merge also runs FTS5 integrity-check on the post-action live
state. These checks keep verification inside SQLite and avoid copying indexed
content into client memory. A failed action or verification rolls the
transaction back; the verified backup is retained once created.
When rollback occurs after backup creation, JSON reports
indexly.performance-action/v1 with mutation_applied: false,
rolled_back: true, backup_retained: true, the backup filename only, and
the error. Preflight or backup failures that never produce a retained verified
snapshot use the ordinary error schema.
Rarely, backup verification or directory synchronization can fail and cleanup
of a partial, sidecar, or final candidate can also fail. The action has not
run, and the named candidate is not a verified backup. JSON uses
indexly.performance-error/v1 with mutation_applied: false,
backup_verified: false, cleanup_incomplete: true, and only
backup_filename. Inspect and remove that named candidate from the configured
backup directory before retrying; do not use it for recovery.
Audit and post-action comparison
Every completed action records action, UTC timestamp, result (applied or
no_op), duration, and numeric-only before/after/delta fields. The numeric
snapshot covers page count, freelist count, schema version, connection change
count, and planner-stat row and byte counts. The checksummed record retains at
most 30 action outcomes.
After commit, Indexly performs a fresh bounded report and requires identity,
schema fingerprint, size bucket, page size, journal mode, and search-index
generation to remain comparable. A size-bucket transition therefore makes the
post-action comparison non-comparable even though the same-database numeric
action audit remains retained. The text output reports the number of
comparable metrics. JSON returns each measured metric’s label, unit, before
value, after value, and delta under postcheck.comparison.
If audit persistence or this post-commit report fails after an applied
outcome, the mutation is not rolled back: it has already committed. The
command retains the verified backup, reports mutation_applied: true, and
exits 3. The corresponding failure after a no_op outcome reports
mutation_applied: false and exits 2.
The action JSON exposes only the retained backup filename, never its full directory path. Like the report, it omits the private identity salt and does not include source paths, roots, filenames from indexed content, content, metadata JSON, query terms, usernames, hostnames, raw logs, or telemetry.
Actions deliberately excluded
Performance maintenance never runs VACUUM, schema migration, FTS rebuild,
cache deletion, tokenizer or prefix changes, external-content FTS conversion,
tag migration, journal-mode changes, or automatic re-indexing. Integrity or
schema failures are repair concerns, not performance eligibility. A
repair_required plan is a Doctor handoff, not permission for perf to repair
or normalize the FTS definition.
For schema drift, corruption, or failed FTS readiness, continue with Indexly Doctor and Database Update & Migration Utilities.
Corporate and Unusual Data
File count, vocabulary size, and average document length do not establish whether a corpus is healthy. Large PDFs, OCR output, source trees, generated material, legal text, and specialized corporate vocabulary can legitimately produce very different values.
The current build does not collect text-volume, document-length, FTS-shadow
allocation, or SQLite dbstat aggregates. A bounded implemented probe that
exhausts its budget reports that the value was not measured; Indexly does not
expand it into an unbounded scan.
Use the report to compare the same installation with itself, then investigate workload and storage changes before taking maintenance action.