Statistics#

KvikIO can report what a run did: how many operations, how many bytes, how long it was busy, and which backend carried the work.

A monitor accumulates for as long as it exists, and get() reads the totals so far.

import cupy

import kvikio


def main(path):
    # Statistics are off until a monitor exists. This one counts every operation below.
    monitor = kvikio.SummaryMonitor()

    a = cupy.arange(100)
    f = kvikio.CuFile(path, "w")
    # Write whole array to file
    f.write(a)
    f.close()

    b = cupy.empty_like(a)
    f = kvikio.CuFile(path, "r")
    # Read whole array from file
    f.read(b)
    assert all(a == b)

    # Use contexmanager
    c = cupy.empty_like(a)
    with kvikio.CuFile(path, "r") as f:
        f.read(c)
    assert all(a == c)

    # Non-blocking read
    d = cupy.empty_like(a)
    with kvikio.CuFile(path, "r") as f:
        future1 = f.pread(d[:50])
        future2 = f.pread(d[50:], file_offset=d[:50].nbytes)
        future1.get()  # Wait for first read
        future2.get()  # Wait for second read
    assert all(a == d)

    # Five calls, one write and four reads, however many reads KvikIO issued underneath.
    # The two `pread()`s above are one operation each, not one per thread-pool task.
    summary = monitor.get()
    assert summary.num_ops == 5
    print(summary)
    print(f"{summary.bytes_transferred} bytes in {summary.num_ops} operations")


if __name__ == "__main__":
    main("/tmp/kvikio-hello-world-file")

Printing a summary, or calling report(), gives a report meant to be read by a person:

KvikIO I/O summary
  wall time            238.22 ms
  busy time            5.66 ms (2.38 % of the wall time)
  busy bandwidth       565.50 kB/s
  operations           5 (4 read, 1 write)
  mean duration        1.13 ms
  bytes                3.12 KiB of 3.12 KiB requested (2.34 KiB read, 800 B written)
  errors               0
  backend POSIX        3.12 KiB in 5 ops, 5.66 ms, 565.50 kB/s
  backend GDS          unused
  backend MMAP         unused
  backend REMOTE_HTTP  unused
  backend REMOTE_HDFS  unused

Busy time and bandwidth#

Busy time is the union of the operations’ spans, so overlapping work counts once and the gaps between calls count as idle.

Busy bandwidth divides the bytes by that rather than by the wall time. A program that reads for 10 ms and then computes for 90 ms is doing I/O at its storage’s speed for a tenth of its life, and dividing by the wall time would report it as ten times slower than it is. Multiply by busy_fraction to recover the whole-span rate.

Getting a summary out of the process#

to_json() gives the same content for anything that would rather parse it, with the timestamps against the wall clock so another program can line the summary up with its own log.

serialize() and deserialize() move a summary between processes. They are exact, so one that has been through a pipe is still a valid previous for since(), and they are what pickling uses.

raw = summary.serialize()
assert kvikio.Summary.deserialize(raw) == summary

The bytes are not a wire format. They are a copy of the C++ struct, readable only by the same architecture and the same build of KvikIO, and anything else is refused rather than misread.

Summaries are not additive. Most fields could be added across processes, but busy time cannot, since two processes are genuinely busy at the same moment.