DEV Community

RowCove
RowCove

Posted on

How to benchmark a large CSV viewer or editor fairly

A large CSV benchmark becomes misleading as soon as two tools are timed doing different jobs.

Displaying a small preview, parsing every record, building a reusable index, filtering the full source, and writing an edited copy are separate operations. A useful comparison defines each one before running the stopwatch.

This tutorial provides a small synthetic correctness fixture and a measurement checklist you can apply to a desktop editor, a CLI, or a data engine.

1. Define the finish line

Task Start Finish
First usable view Request to open the file Agreed number of records painted and controls usable
Complete scan Open request for a fresh application session Every CSV record parsed and the exact count returned
Filter from unopened source Open request All matching source records identified and the first result page usable
Filter after opening Query submission Complete result count and usable result page
Save an edited copy Save request Output closed successfully, ready for a separate reopen check

Record end-to-end time and any useful internal phases. Include required detection, decompression, indexing and query preparation in the end-to-end number.

A timer ending when a backend returns data is useful, but label it separately from time to visible, usable results.

2. Make correctness predictable

Start with a generated file whose answers are known. The example below includes commas, quotes and embedded newlines. These matter: a physical line count is not necessarily a CSV record count.

The rule is deliberately simple: record IDs divisible by 97 have status review. For N records, the expected match count is:

floor((N - 1) / 97) + 1
Enter fullscreen mode Exit fullscreen mode

The script uses Python's standard CSV reader and writer. It processes records incrementally instead of collecting the file into a list. Files are opened with newline='', as the Python CSV documentation recommends.

Save this as csv-workflow-example.py and run it in a new directory:

import csv
import sys
from pathlib import Path
from time import perf_counter

rows = int(sys.argv[1]) if len(sys.argv) > 1 else 100_000
if rows < 1:
    raise ValueError('rows must be positive')
source = Path('mock-records.csv')
output = Path('mock-review.csv')

start = perf_counter()
with source.open('x', encoding='utf-8', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['record_id', 'status', 'amount_cents', 'note'])
    for i in range(rows):
        note = 'synthetic, quoted "value"'
        if i % 1000 == 0:
            note += '\nsecond line'
        writer.writerow([i, 'review' if i % 97 == 0 else 'ready', i % 100_000, note])
print(f'generate: {perf_counter() - start:.3f}s; bytes={source.stat().st_size}')

start = perf_counter()
scanned = matched = 0
with source.open(encoding='utf-8', newline='') as src, output.open('x', encoding='utf-8', newline='') as dst:
    reader = csv.DictReader(src)
    writer = csv.DictWriter(dst, fieldnames=reader.fieldnames)
    writer.writeheader()
    for row in reader:
        scanned += 1
        if row['status'] == 'review':
            writer.writerow(row)
            matched += 1
print(f'scan + filter + write: {perf_counter() - start:.3f}s; rows={scanned}; matches={matched}')

start = perf_counter()
with output.open(encoding='utf-8', newline='') as f:
    records = csv.DictReader(f)
    actual = 0
    for row in records:
        assert row['status'] == 'review'
        assert int(row['record_id']) == actual * 97
        actual += 1
assert scanned == rows
assert actual == matched == (rows - 1) // 97 + 1
print(f'verify output: {perf_counter() - start:.3f}s; OK')
Enter fullscreen mode Exit fullscreen mode

Run a small correctness check first:

python csv-workflow-example.py 10000
Enter fullscreen mode Exit fullscreen mode

That run should scan 10,000 records and export 104 matches. The example was executed locally with those inputs and all assertions passed. Its small-file timings are not a large-file performance claim.

The default is 100,000 records. Change the row-count argument to build a larger fixture, then report its actual byte size. The script uses exclusive creation mode, so it will not overwrite existing files with the same names.

Generation, scan/filter/write and verification have separate timers. The filtering timer includes writing the output; it should not be compared directly with an in-memory query timer.

3. Keep opening and query costs visible

Measure both of these workflows:

  • Fresh session: open source → detect format → prepare required state → run query → show results.
  • Already open: submit query → show results.

Keep the same columns, comparison semantics, match count and requested page size. A text search for review across the whole file is a different job from exact equality on the status column.

For each result, inspect the first page, a middle page and the last page. Check source record IDs as well as the match count.

4. Test editing independently

Use a copy of the fixture and define one exact correction, such as changing record ID 97 from review to checked.

Then:

  1. Save to a distinct output file.
  2. Reopen the output with an independent CSV reader.
  3. Verify the corrected record and unchanged record count.
  4. Verify commas, quotes and embedded newlines still round-trip.
  5. Confirm the original source is unchanged.

Report save time and reopen/validation time separately. If the user-facing task includes both, report their combined duration too.

For sorting, compare numeric IDs numerically, verify every output record appears exactly once, and define a tie-breaker for duplicate sort keys. Looking at the first ten rows cannot establish that a complete sort is correct.

5. Measure resources for the whole job

Record:

  • Peak memory for the application's full process tree.
  • Temporary-directory growth, including decompressed files and indexes.
  • Output-copy size separately from temporary working space.
  • Time to navigate to an already-defined page after filtering or sorting.
  • Cancellation latency and whether incomplete outputs are distinguishable.

A tool that returns quickly but leaves a large background job running has not necessarily completed the same task as another tool.

6. Repeat with an explicit cache policy

Use at least three matched runs and publish the individual values plus the median. Record the OS, application version, CPU, RAM, storage and source location.

Restarting an application does not establish that the operating-system file cache is cold. Describe the cache policy you actually used. Keep earlier runs visible when a later run benefits from a reusable index.

A useful results sheet has columns for:

tool/version | source bytes | records | cache/index state
task | preparation | execution | usable-results total
peak process-tree memory | temp peak | output bytes
correctness check | cancellation outcome | notes
Enter fullscreen mode Exit fullscreen mode

If a paid feature is unavailable, label it not tested: license unavailable. If a tool stops responding, describe the action, observation interval and resource behavior; do not invent a completion time.

The aim is to identify which complete workflow saves time on your data, with results you can reproduce and trust.


Authorship disclosure: This tutorial was drafted by Myself. The synthetic Python example was executed locally to check the stated record counts. No private datasets or customer information were used.

Top comments (0)