Memory and performance tuning

The defaults are chosen from measurements, and most users should not need to change them. This page says what they are, why, and when to override them.

Threads

DuckDB’s own default is one thread per core. gffbase uses at most 8 – for ingest and for a handle it opens on a file – because more bought no time and cost memory: MANE ingest took 15.8-16.1 s at 8, 32 and 128 threads alike, while peak RSS rose from 1.0 GiB to 1.2 and 1.6 GiB, and every single-call query shape (children, parents, db[id], region) ran 10-24% faster at 8 threads than at 128.

GFFBASE_THREADS=4 python my_script.py    # any connection gffbase opens

create_db(..., pragmas={"threads": n}) sets it for one ingest, and db.set_pragmas({"threads": n}) for an open handle. A connection you open yourself and pass in keeps its own setting.

Memory during ingest

Ingest into a file runs under a DuckDB memory budget of 512 MB. A step that needs more – building the primary key or an index over tens of millions of ids – is retried with the budget doubled, up to DuckDB’s own limit, and checkpoints are taken at step boundaries so none of them can run short. The connection you get back uses DuckDB’s default.

Ingest wall time and peak resident memory at the defaults (8 threads, ingest only, median of repeated runs on the benchmark machine):

Corpus

Features

Ingest

Peak RSS

MANE v1.5

0.5 M

14.9 s

1.0 GiB

CHESS 3.1.3

2.8 M

36.2 s

1.6 GiB

RefSeq GRCh38.p14

4.9 M

119 s

2.0 GiB

GENCODE v49 GTF

6.1 M

209 s

2.6 GiB

GENCODE v49 GFF3

6.1 M

192 s

2.8 GiB

To hold DuckDB to a fixed limit instead, set it yourself – the budget then stays out of the way:

limited = gffbase.create_db(HIERARCHY_PATH, "limited.duckdb", force=True,
                            pragmas={"memory_limit": "4GB"})

Too low a fixed limit can fail an index build, which cannot spill: FlyBase’s primary key over 31.8 M ids needs about 2 GB. An in-memory database (":memory:") is never budgeted, since its tables cannot be evicted.

Where ingest time goes

gffbase.ingest.from_file returns IngestStats; its stages field holds wall seconds per stage, in order – parse, append, primary_key, gtf_inference or edges, closure, indexes, and the rest. GFFBASE_INGEST_TRACE=1 prints each stage as it ends, with the process’s peak RSS so far:

$ GFFBASE_INGEST_TRACE=1 gffbase create gencode.v49.gff3.gz gencode.duckdb --merge create_unique
gffbase ingest: setup 0.128 s (peak RSS 0.13 GiB)
gffbase ingest: parse 57.8 s (peak RSS 1.78 GiB)
gffbase ingest: append 94.2 s (peak RSS 1.78 GiB)
...

benchmarks/07_profile.py sweeps threads and batch sizes and captures the query plans.

Loops, and when to batch

A children(), parents() or db[id] call on a feature that an open iterator holds is prefetched: it is answered, with its neighbours’, by one query, and the loop runs within 1.4-3.5x of gffutils’ speed.

# Prefetched: the genes come from a gffbase iterator.
for gene in db.features_of_type("gene"):
    for transcript in db.children(gene, level=1):
        exons = list(db.children(transcript, featuretype="exon"))

A call on an id from your own list is a DuckDB query of its own – about 1.3 ms for children() and 0.5 ms for db[id], against a few hundredths of a millisecond for SQLite. For thousands of ids, ask once:

my_transcript_ids = [t.id for t in db.features_of_type("mRNA")]
exons = db.children_batched(my_transcript_ids, featuretype="exon", format="arrow")

Prefetch applies to the default ordering with a level and an optional featuretype; order_by, reverse and limit use the per-call path. Answers are identical either way. Writes through the FeatureDB – update, delete, add_relation – are seen at once; SQL run directly on db.conn inside a loop is not seen by that loop’s prefetched answers. benchmarks/08_loops.py times the loops against gffutils.

Disk

A gffbase database is about half the size it was in 0.2 (GENCODE v49 GFF3: 3.7 GB), smaller than gffutils’ SQLite file on the same input. The attributes(feature_id) index is built on the first write that needs it, so a database that is never modified never stores it.