Repository: Monotype/c2pa-font-handler
Pull request #42: C2PA-678: Add benchmarks for SFNT and WOFF1.
Author: login: brogdonm
Labels: name: documentation
description: Improvements or additions to documentation
name: enhancement
description: New feature or request
name: devops
description: Development operations
Created: 2025-05-14T14:54:19Z | Merged: 2025-05-14T20:25:10Z

Fix up some small typos.

Add documentation about profiling and benchmarking.

<!--  Title should use the format: "ISSUE-123: Summary of change"   -->
<!--     Branch names for Stories: "feature/ISSUE-123/featureName"  -->
<!--        Branch names for Bugs: "fix/ISSUE-123/bugFixName"       -->
# Issue(s)

- C2PA-678

# Checklist
<!--  Replace the ' ' with an 'x' for each that applies:  -->
- [X] **Merge Commit** will be updated with `(MAJOR)` | `(MINOR)` when appropriate
- [X] **PR labeled** appropriately
- [X] Changes include a single fix/feature
- [X] **Documentation** updated:
  - [X] Developer documentation (ReadMe files)
  - [ ] Product documentation (Release notes, PDK Guide, Functional Spec, API documents, ...)
- [X] **Test case(s)** added
  - [X] Unit Tests

# Notes for Reviewers
<!--  Information to assist code reviewers:                    -->
<!--    Dependencies or co-reqs (other branches, other repos)  -->
<!--    Limitations and/or breakage.                           -->

Adds benchmarking using the `criterion` crate with support for profiling using the `dhat` crate.

# Verification Instructions
<!-- Instructions for checking or testing this change. -->

Should be able to follow instructions in the `CONTRIBUTING.md` to perform benchmarks.


Diff:
diff --git a/.cspell.json b/.cspell.json
index 5dd1539..18cf2e8 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -21,8 +21,11 @@
     "byteorder",
     "cdef",
     "cksum",
+    "cpuset",
     "decompressor",
+    "dhat",
     "DSIG",
+    "flamegraph",
     "flate",
     "fullword",
     "FWORD",
@@ -36,6 +39,7 @@
     "rustup",
     "sfnt",
     "Stubber",
+    "taskset",
     "thiserror",
     "toolset",
     "xdadau"
diff --git a/.devtools/faux_c2pa_table.bin b/.devtools/faux_c2pa_table.bin
new file mode 100644
index 0000000..77abfeb
Binary files /dev/null and b/.devtools/faux_c2pa_table.bin differ
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a7e8d73..cb64fd8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,6 +11,8 @@
   - [main](#main)
 - [Documentation](#documentation)
 - [Unit Test Coverage](#unit-test-coverage)
+- [Benchmarking](#benchmarking)
+  - [Profiling](#profiling)
 - [Merging](#merging)
   - [Auto Versioning](#auto-versioning)
 - [Git Hooks](#git-hooks)
@@ -119,6 +121,41 @@ should include with:
 mod tests;
 ```
 
+## Benchmarking
+
+This project uses `criterion` as the benchmarking framework. Criterion provides statistically sound performance measurements, including throughput estimates and confidence intervals, and works on stable Rust.
+
+To run benchmarks:
+
+```bash
+cargo bench
+```
+
+This will execute any benchmarks defined in the [c2pa-font-handler/Cargo.toml](./c2pa-font-handler/Cargo.toml) file and output performance summaries to the console. Criterion also generates detailed reports, including time distributions and trend analysis, stored in the `target/criterion` directory.
+
+> Note on Accuracy: Benchmark results can vary depending on current system load, CPU frequency scaling, and background tasks. For more stable and repeatable outcomes, run benchmarks on an idle system with consistent CPU performance settings (e.g., disabling turbo boost or using `taskset`/`cpuset` on Linux to isolate cores).
+
+### Profiling
+
+For memory profiling of benchmarks, this project supports integration with the `dhat` heap profiler. This allows tracking allocations during benchmarking runs.
+
+To profile, the only difference is how the benches are ran. For example the following will
+perform profiling of the WOFF1 benchmarks:
+
+```bash
+cargo test --profile release --bench woff1 --features="woff" -- --profile-time 3 --bench
+```
+
+And then to convert the output to a human-readable format, you can use the
+`dhat-to-flamegraph` command line tool:
+
+```bash
+# Install the dhat-to-flamegraph tool
+cargo install dhat-to-flamegraph
+# Convert the dhat output to a flamegraph
+dhat-to-flamegraph  path/to/dhat-heap.json --output path/to/dhat-heap.svg
+```
+
 ## Merging
 
 Once the PR has been reviewed, approved & passes all automated checks, it will
diff --git a/Cargo.lock b/Cargo.lock
index c77018d..e169c9d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -26,6 +26,12 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
 [[package]]
 name = "anstream"
 version = "0.6.18"
@@ -108,6 +114,12 @@ version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
 [[package]]
 name = "byteorder"
 version = "1.5.0"
@@ -128,6 +140,8 @@ dependencies = [
  "byteorder",
  "bytes",
  "clap",
+ "criterion",
+ "dhat",
  "flate2",
  "serde",
  "serde_json",
@@ -138,6 +152,12 @@ dependencies = [
  "tracing-test",
 ]
 
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
 [[package]]
 name = "cc"
 version = "1.2.17"
@@ -153,6 +173,33 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
 [[package]]
 name = "clap"
 version = "4.5.23"
@@ -217,6 +264,95 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "criterion"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+dependencies = [
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "is-terminal",
+ "itertools",
+ "num-traits",
+ "once_cell",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
+[[package]]
+name = "dhat"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cd11d84628e233de0ce467de10b8633f4ddaecafadefc86e13b84b8739b827"
+dependencies = [
+ "backtrace",
+ "lazy_static",
+ "mintex",
+ "parking_lot",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+ "thousands",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
 [[package]]
 name = "flate2"
 version = "1.1.0"
@@ -234,24 +370,70 @@ version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 
+[[package]]
+name = "half"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
 [[package]]
 name = "heck"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hermit-abi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "is_terminal_polyfill"
 version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
 
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.5.0"
@@ -314,6 +496,12 @@ dependencies = [
  "adler2",
 ]
 
+[[package]]
+name = "mintex"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536"
+
 [[package]]
 name = "mio"
 version = "1.0.3"
@@ -335,6 +523,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "object"
 version = "0.36.5"
@@ -350,6 +547,12 @@ version = "1.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
 
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -385,6 +588,34 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
 
+[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.95"
@@ -403,6 +634,26 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.5.7"
@@ -462,12 +713,33 @@ version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
 
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustversion"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
 [[package]]
 name = "ryu"
 version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
@@ -583,6 +855,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "thousands"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
+
 [[package]]
 name = "thread_local"
 version = "1.1.8"
@@ -593,6 +871,16 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "tokio"
 version = "1.44.2"
@@ -735,12 +1023,90 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -757,6 +1123,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
diff --git a/Cargo.toml b/Cargo.toml
index 8b11ddd..5f20a2f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ description = "C2PA Font Handler (or codec) to be used with the c2pa-rs SDK."
 anyhow = { version = "1.0.94" }
 byteorder = { version = "1.5.0" }
 bytes = { version = "1.9.0" }
+criterion = { version = "0.5.1", features = ["html_reports"]}
 clap = { version = "4.5.23", features = ["derive"] }
 flate2 = { version = "1.1.0", features = ["zlib-ng"], default-features = false }
 serde = { version = "1.0.218", features = ["derive"] }
@@ -48,3 +49,6 @@ broken_intra_doc_links = "deny"
 private_intra_doc_links = "deny"
 bare_urls = "deny"
 unescaped_backticks = "deny"
+
+[profile.bench]
+debug = true
\ No newline at end of file
diff --git a/c2pa-font-handler/Cargo.toml b/c2pa-font-handler/Cargo.toml
index 2a290df..1942a14 100644
--- a/c2pa-font-handler/Cargo.toml
+++ b/c2pa-font-handler/Cargo.toml
@@ -41,6 +41,8 @@ tracing.workspace = true
 
 [dev-dependencies]
 clap.workspace = true
+criterion.workspace = true
+dhat = {version = "0.3.3" }
 tokio.workspace = true
 tracing-subscriber.workspace = true
 tracing-test.workspace = true
@@ -51,3 +53,12 @@ workspace = true
 [[example]]
 name = "woff1"
 required-features = ["woff"]
+
+[[bench]]
+name = "sfnt"
+harness = false
+
+[[bench]]
+name = "woff1"
+required-features = ["woff"]
+harness = false
\ No newline at end of file
diff --git a/c2pa-font-handler/benches/sfnt.rs b/c2pa-font-handler/benches/sfnt.rs
new file mode 100644
index 0000000..26c6b2e
--- /dev/null
+++ b/c2pa-font-handler/benches/sfnt.rs
@@ -0,0 +1,189 @@
+// Copyright 2025 Monotype Imaging Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+#![allow(missing_docs)]
+
+#[path = "utils/profiler.rs"]
+mod profiler_utils;
+use profiler_utils::DhatProfiler;
+#[path = "utils/c2pa.rs"]
+mod c2pa_utils;
+use c2pa_utils::*;
+
+#[path = "utils/sfnt.rs"]
+mod sfnt_utils;
+use c2pa_font_handler::{
+    c2pa::{C2PASupport, UpdatableC2PA},
+    chunks::ChunkReader,
+    sfnt::{
+        directory::{SfntDirectory, SfntDirectoryEntry},
+        font::SfntFont,
+        header::SfntHeader,
+        table::TableC2PA,
+    },
+    FontDataExactRead, FontDataRead, FontDataWrite,
+};
+use criterion::{criterion_group, criterion_main, Criterion};
+use sfnt_utils::*;
+
+/// Collection of benchmarks for SFNT font data reading and writing.
+fn sfnt_font_benchmarks(c: &mut Criterion) {
+    // Benchmark for reading the font data
+    c.bench_function("parse_sfnt_from_reader", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(get_sfnt_font_data());
+            let _ = SfntFont::from_reader(&mut font_stream)
+                .expect("Failed to read font data");
+        });
+    });
+
+    // Benchmark the process of getting chunk positions
+    c.bench_function("get_chunk_positions", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(get_sfnt_font_data());
+            let _ = SfntFont::get_chunk_positions(&mut font_stream)
+                .expect("Failed to get chunk positions");
+        });
+    });
+
+    // Benchmark the process of mimicking the C2PA read/write process of a
+    // typical signing flow.
+    c.bench_function("sfnt_c2pa_readwrite_mimic", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(get_sfnt_font_data());
+            let mut dest_stream = std::io::Cursor::new(Vec::new());
+
+            // Simulate the removal of the C2PA record
+            parse_font_and_execute_write(&mut font_stream, Some(&mut dest_stream), |font| {
+                if font.has_c2pa() {
+                   font.remove_c2pa_record()
+                       .expect("Failed to remove C2PA record");
+                }
+                let update_record = c2pa_font_handler::c2pa::UpdateContentCredentialRecord::builder()
+                    .with_active_manifest_uri("urn:monotype:example".to_string())
+                    .build();
+                font.update_c2pa_record(update_record)
+                    .expect("Failed to update C2PA record");
+            });
+
+            // Get the chunk positions, done by the SDK to determine general box positions
+            font_stream.set_position(0);
+            let _ = SfntFont::get_chunk_positions(&mut font_stream)
+                .expect("Failed to get chunk positions");
+
+            // Simulate adding new C2PA record
+            let mut dest_stream = std::io::Cursor::new(Vec::new());
+            font_stream.set_position(0);
+            parse_font_and_execute_write(&mut font_stream, Some(&mut dest_stream), |font| {
+                let record = c2pa_font_handler::c2pa::UpdateContentCredentialRecord::builder()
+                    .with_active_manifest_uri("urn:monotype:example".to_string())
+                    .build();
+                font.update_c2pa_record(record)
+                    .expect("Failed to add C2PA record");
+            });
+
+            // Simulate reading C2PA for validation
+            let mut font_stream = std::io::Cursor::new(dest_stream.get_ref().as_slice());
+            parse_font_and_execute(&mut font_stream, |font| {
+                font.get_c2pa()
+                    .expect("Failed to read C2PA record");
+            });
+        });
+    });
+}
+
+/// Collection of benchmarks for SFNT header reading and writing.
+fn sfnt_header_benchmarks(c: &mut Criterion) {
+    let sfnt_data = get_sfnt_header_data();
+
+    c.bench_function("sfnt_header_from_reader", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(sfnt_data);
+            let _ = SfntHeader::from_reader(&mut font_stream)
+                .expect("Failed to read SFNT header");
+        });
+    });
+
+    c.bench_function("sfnt_header_from_reader_exact", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(sfnt_data);
+            let _header =
+                SfntHeader::from_reader_exact(&mut font_stream, 0, 12)
+                    .expect("Failed to read SFNT header");
+        });
+    });
+
+    c.bench_function("sfnt_header_write", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(Vec::new());
+            let header = get_sfnt_header();
+            header
+                .write(&mut font_stream)
+                .expect("Failed to write SFNT header");
+        });
+    });
+}
+
+/// Collection of benchmarks for SFNT directory reading and writing.
+fn sfnt_directory_benchmarks(c: &mut Criterion) {
+    c.bench_function("sfnt_directory_from_reader", |b| {
+        b.iter(|| {
+            let mut font_stream =
+                std::io::Cursor::new(get_sfnt_directory_data());
+            let _ = SfntDirectory::from_reader_exact(
+                &mut font_stream,
+                0,
+                size_of::<SfntDirectoryEntry>() * FONT_OTF_DIRECTORY_ENTRIES,
+            )
+            .expect("Failed to read SFNT font data");
+        });
+    });
+    c.bench_function("sfnt_directory_write", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(Vec::new());
+            let directory = get_sfnt_directory();
+            directory
+                .write(&mut font_stream)
+                .expect("Failed to write SFNT directory");
+        });
+    });
+}
+
+/// Collection of benchmarks for SFNT table reading and writing.
+fn sfnt_table_benchmarks(c: &mut Criterion) {
+    c.bench_function("sfnt_table_read", |b| {
+        b.iter(|| {
+            let (data, length) = get_faux_c2pa_table_data();
+            let mut font_stream = std::io::Cursor::new(data);
+            let _ = TableC2PA::from_reader_exact(&mut font_stream, 0, length)
+                .expect("Failed to read SFNT font data");
+        });
+    });
+    c.bench_function("sfnt_table_write", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(Vec::new());
+            let table = get_faux_c2pa_table();
+            table
+                .write(&mut font_stream)
+                .expect("Failed to write SFNT table");
+        });
+    });
+}
+
+criterion_group!(
+    name = benches;
+    config = Criterion::default().with_profiler(DhatProfiler::new());
+    targets =  sfnt_font_benchmarks, sfnt_header_benchmarks, sfnt_directory_benchmarks, sfnt_table_benchmarks,
+);
+criterion_main!(benches);
diff --git a/c2pa-font-handler/benches/utils/c2pa.rs b/c2pa-font-handler/benches/utils/c2pa.rs
new file mode 100644
index 0000000..2ab3c4a
--- /dev/null
+++ b/c2pa-font-handler/benches/utils/c2pa.rs
@@ -0,0 +1,39 @@
+// Copyright 2025 Monotype Imaging Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+//! Various utilities for C2PA table data for benchmarks.
+
+use std::sync::OnceLock;
+
+use c2pa_font_handler::{sfnt::table::TableC2PA, FontDataExactRead};
+
+/// Static lock around the faux C2PA table.
+pub static SFNT_FAUX_C2PA_TABLE: OnceLock<TableC2PA> = OnceLock::new();
+
+/// Gets data for a faux C2PA table from the file system.
+pub fn get_faux_c2pa_table_data() -> (&'static [u8], usize) {
+    let data = include_bytes!("../../../.devtools/faux_c2pa_table.bin");
+    let length = data.len();
+    (data, length)
+}
+
+/// Gets the faux C2PA table from the file system.
+pub fn get_faux_c2pa_table() -> &'static TableC2PA {
+    SFNT_FAUX_C2PA_TABLE.get_or_init(|| {
+        let (data, length) = get_faux_c2pa_table_data();
+        let mut font_stream = std::io::Cursor::new(data);
+        TableC2PA::from_reader_exact(&mut font_stream, 0, length)
+            .expect("Failed to read faux C2PA table")
+    })
+}
diff --git a/c2pa-font-handler/benches/utils/profiler.rs b/c2pa-font-handler/benches/utils/profiler.rs
new file mode 100644
index 0000000..c0f4513
--- /dev/null
+++ b/c2pa-font-handler/benches/utils/profiler.rs
@@ -0,0 +1,74 @@
+// Copyright 2025 Monotype Imaging Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+//! Profiler for benchmarking memory usage, using the `dhat` crate.
+
+use criterion::profiler::Profiler;
+
+/// Global allocator for profiling memory usage in benchmarks.
+#[global_allocator]
+pub static ALLOC: dhat::Alloc = dhat::Alloc;
+
+/// A profiler to use with the dhat crate.
+///
+/// # Remarks
+/// This profiler is intended to be used with the `criterion` crate for
+/// benchmarking. It uses the `dhat` crate to profile memory usage during
+/// benchmarks. To use it in your benchmarks, you need to set the `--profile`
+/// flag when running the benchmarks. For example, to profile for 3 seconds
+/// using the sfnt_c2pa_readwrite_mimic:
+///
+/// ```bash
+/// cargo test --profile release --bench sfnt -- --profile-time 3 --bench sfnt_c2pa_readwrite_mimic
+/// ```
+///
+/// And then to convert the output to a human-readable format, you can use the
+/// `dhat-to-flamegraph` command line tool:
+///
+/// ```bash
+/// # Install the dhat-to-flamegraph tool
+/// cargo install dhat-to-flamegraph
+/// # Convert the dhat output to a flamegraph
+/// dhat-to-flamegraph  path/to/dhat-heap.json --output <output_file>
+/// ```
+pub struct DhatProfiler {
+    _profiler: Option<dhat::Profiler>,
+}
+
+impl DhatProfiler {
+    /// Creates a new [`DhatProfiler`].
+    pub fn new() -> Self {
+        Self { _profiler: None }
+    }
+}
+
+// Implement the `Profiler` trait for `DhatProfiler`.
+impl Profiler for DhatProfiler {
+    fn start_profiling(
+        &mut self,
+        _benchmark_id: &str,
+        _benchmark_dir: &std::path::Path,
+    ) {
+        self._profiler = Some(dhat::Profiler::new_heap());
+    }
+
+    fn stop_profiling(
+        &mut self,
+        _benchmark_id: &str,
+        _benchmark_dir: &std::path::Path,
+    ) {
+        // Drop the profiler to write the report
+        self._profiler = None;
+    }
+}
diff --git a/c2pa-font-handler/benches/utils/sfnt.rs b/c2pa-font-handler/benches/utils/sfnt.rs
new file mode 100644
index 0000000..158d75e
--- /dev/null
+++ b/c2pa-font-handler/benches/utils/sfnt.rs
@@ -0,0 +1,115 @@
+// Copyright 2025 Monotype Imaging Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+//! Various utilities for reading and writing SFNT font data for benchmarks.
+
+use std::{
+    io::{Read, Seek, Write},
+    sync::OnceLock,
+};
+
+use c2pa_font_handler::{
+    sfnt::{
+        directory::{SfntDirectory, SfntDirectoryEntry},
+        font::SfntFont,
+        header::SfntHeader,
+    },
+    FontDataExactRead, FontDataRead, MutFontDataWrite,
+};
+
+/// The number of entries in the SFNT font directory.
+///
+/// # Remarks
+/// This is a constant value that represents the number of entries in the
+/// SFNT font directory of the font loaded by `get_sfnt_font_data()`.
+pub const FONT_OTF_DIRECTORY_ENTRIES: usize = 11;
+
+/// Static lock around the SFNT font data.
+pub static SFNT_FONT_DATA: OnceLock<Vec<u8>> = OnceLock::new();
+
+/// Static lock around the SFNT font directory.
+pub static SFNT_DIRECTORY: OnceLock<SfntDirectory> = OnceLock::new();
+
+/// Static lock around the SFNT font header.
+pub static SFNT_HEADER: OnceLock<SfntHeader> = OnceLock::new();
+
+/// Gets the SFNT font data from the file system.
+pub fn get_sfnt_font_data() -> &'static [u8] {
+    SFNT_FONT_DATA
+        .get_or_init(|| include_bytes!("../../../.devtools/font.otf").to_vec())
+}
+
+/// Gets the SFNT font header data from the file system.
+pub fn get_sfnt_header_data() -> &'static [u8] {
+    &get_sfnt_font_data()[..size_of::<SfntHeader>()]
+}
+
+/// Gets the SFNT font header from the file system.
+pub fn get_sfnt_header() -> &'static SfntHeader {
+    SFNT_HEADER.get_or_init(|| {
+        let mut font_stream = std::io::Cursor::new(get_sfnt_header_data());
+        SfntHeader::from_reader(&mut font_stream)
+            .expect("Failed to read font header")
+    })
+}
+
+/// Gets the SFNT font directory data from the file system.
+pub fn get_sfnt_directory_data() -> &'static [u8] {
+    &get_sfnt_font_data()[size_of::<SfntHeader>()..]
+}
+
+/// Gets the SFNT font directory from the file system.
+pub fn get_sfnt_directory() -> &'static SfntDirectory {
+    SFNT_DIRECTORY.get_or_init(|| {
+        let mut font_stream = std::io::Cursor::new(get_sfnt_directory_data());
+        SfntDirectory::from_reader_exact(
+            &mut font_stream,
+            0,
+            size_of::<SfntDirectoryEntry>() * FONT_OTF_DIRECTORY_ENTRIES,
+        )
+        .expect("Failed to read font directory")
+    })
+}
+
+/// Parses the data in the font stream as a [`SfntFont`] and executes the
+/// provided function with the parsed font data object.
+pub fn parse_font_and_execute<F, S>(font_stream: &mut S, execute: F)
+where
+    S: Read + Seek + ?Sized,
+    F: FnOnce(SfntFont),
+{
+    execute(
+        SfntFont::from_reader(font_stream).expect("Failed to read font data"),
+    );
+}
+
+/// Parses the data in the font stream as a [`SfntFont`] and executes the
+/// provided function with the parsed font data object, then writes the modified
+/// font data to the destination stream if provided.
+pub fn parse_font_and_execute_write<F, S, W>(
+    font_stream: &mut S,
+    dest: Option<&mut W>,
+    execute: F,
+) where
+    S: Read + Seek + ?Sized,
+    W: Write + ?Sized,
+    F: FnOnce(&mut SfntFont),
+{
+    let mut font =
+        SfntFont::from_reader(font_stream).expect("Failed to read font data");
+    execute(&mut font);
+    if let Some(dest) = dest {
+        font.write(dest).expect("Failed to write font data");
+    }
+}
diff --git a/c2pa-font-handler/benches/utils/woff1.rs b/c2pa-font-handler/benches/utils/woff1.rs
new file mode 100644
index 0000000..62e764e
--- /dev/null
+++ b/c2pa-font-handler/benches/utils/woff1.rs
@@ -0,0 +1,117 @@
+// Copyright 2025 Monotype Imaging Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+//! Various utilities for reading and writing WOFF1 font data for benchmarks.
+
+use std::{
+    io::{Read, Seek, Write},
+    sync::OnceLock,
+};
+
+use c2pa_font_handler::{
+    woff1::{
+        directory::{Woff1Directory, Woff1DirectoryEntry},
+        font::Woff1Font,
+        header::Woff1Header,
+    },
+    FontDataExactRead, FontDataRead, MutFontDataWrite,
+};
+
+/// The number of entries in the WOFF1 font directory.
+///
+/// # Remarks
+/// This is a constant value that represents the number of entries in the
+/// WOFF1 font directory of the font loaded by `get_woff1_font_data()`.
+pub const FONT_WOFF1_DIRECTORY_ENTRIES: usize = 10;
+
+/// Static lock around the WOFF1 font data.
+pub static WOFF1_FONT_DATA: OnceLock<Vec<u8>> = OnceLock::new();
+
+/// Static lock around the WOFF1 font directory.
+pub static WOFF1_DIRECTORY: OnceLock<Woff1Directory> = OnceLock::new();
+
+/// Static lock around the WOFF1 font header.
+pub static WOFF1_HEADER: OnceLock<Woff1Header> = OnceLock::new();
+
+/// Gets the WOFF1 font data from the file system.
+pub fn get_woff1_font_data() -> &'static [u8] {
+    WOFF1_FONT_DATA
+        .get_or_init(|| include_bytes!("../../../.devtools/font.woff").to_vec())
+}
+
+/// Gets the WOFF1 font header data from the file system.
+pub fn get_woff1_header_data() -> &'static [u8] {
+    &get_woff1_font_data()[..size_of::<Woff1Header>()]
+}
+
+/// Gets the WOFF1 font header from the file system.
+pub fn get_woff1_header() -> &'static Woff1Header {
+    WOFF1_HEADER.get_or_init(|| {
+        let mut font_stream = std::io::Cursor::new(get_woff1_header_data());
+        Woff1Header::from_reader(&mut font_stream)
+            .expect("Failed to read font header")
+    })
+}
+
+/// Gets the WOFF1 font directory data from the file system.
+pub fn get_woff1_directory_data() -> &'static [u8] {
+    &get_woff1_font_data()[size_of::<Woff1Header>()..]
+}
+
+/// Gets the WOFF1 font directory from the file system.
+pub fn get_woff1_directory() -> &'static Woff1Directory {
+    WOFF1_DIRECTORY.get_or_init(|| {
+        let mut font_stream = std::io::Cursor::new(get_woff1_directory_data());
+        Woff1Directory::from_reader_exact(
+            &mut font_stream,
+            0,
+            size_of::<Woff1DirectoryEntry>() * FONT_WOFF1_DIRECTORY_ENTRIES,
+        )
+        .expect("Failed to read font directory")
+    })
+}
+
+/// Parses the data in the font stream as a [`Woff1Font`] and executes the
+/// provided function with the parsed font data object.
+#[allow(dead_code)] // Remove this when C2PAUpdate has been implemented for Woff1Font
+pub fn parse_font_and_execute<F, S>(font_stream: &mut S, execute: F)
+where
+    S: Read + Seek + ?Sized,
+    F: FnOnce(Woff1Font),
+{
+    execute(
+        Woff1Font::from_reader(font_stream).expect("Failed to read font data"),
+    );
+}
+
+/// Parses the data in the font stream as a [`Woff1Font`] and executes the
+/// provided function with the parsed font data object, then writes the modified
+/// font data to the destination stream if provided.
+#[allow(dead_code)] // Remove this when C2PAUpdate has been implemented for Woff1Font
+pub fn parse_font_and_execute_write<F, S, W>(
+    font_stream: &mut S,
+    dest: Option<&mut W>,
+    execute: F,
+) where
+    S: Read + Seek + ?Sized,
+    W: Write + ?Sized,
+    F: FnOnce(&mut Woff1Font),
+{
+    let mut font =
+        Woff1Font::from_reader(font_stream).expect("Failed to read font data");
+    execute(&mut font);
+    if let Some(dest) = dest {
+        font.write(dest).expect("Failed to write font data");
+    }
+}
diff --git a/c2pa-font-handler/benches/woff1.rs b/c2pa-font-handler/benches/woff1.rs
new file mode 100644
index 0000000..8c85bca
--- /dev/null
+++ b/c2pa-font-handler/benches/woff1.rs
@@ -0,0 +1,184 @@
+// Copyright 2025 Monotype Imaging Inc.
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+
+#![allow(missing_docs)]
+//#![cfg(feature = "woff")]
+
+#[path = "utils/profiler.rs"]
+mod profiler_utils;
+use c2pa_font_handler::{
+    chunks::ChunkReader,
+    sfnt::table::TableC2PA,
+    woff1::{
+        directory::{Woff1Directory, Woff1DirectoryEntry},
+        font::Woff1Font,
+        header::Woff1Header,
+    },
+    FontDataExactRead, FontDataRead, FontDataWrite,
+};
+use criterion::{criterion_group, criterion_main, Criterion};
+use profiler_utils::DhatProfiler;
+
+#[path = "utils/c2pa.rs"]
+mod c2pa_utils;
+use c2pa_utils::*;
+#[path = "utils/woff1.rs"]
+mod woff1_utils;
+use woff1_utils::*;
+
+fn woff1_font_benchmarks(c: &mut Criterion) {
+    // Benchmark for reading the font data
+    c.bench_function("parse_woff1_from_reader", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(get_woff1_font_data());
+            let _ = Woff1Font::from_reader(&mut font_stream)
+                .expect("Failed to read font data");
+        });
+    });
+
+    // Benchmark the process of getting chunk positions
+    c.bench_function("get_chunk_positions", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(get_woff1_font_data());
+            let _ = Woff1Font::get_chunk_positions(&mut font_stream)
+                .expect("Failed to get chunk positions");
+        });
+    });
+
+    /* // UNCOMMENT THIS BLOCK TO TEST ONCE WOFF1 supports updating C2PA
+    // Benchmark the process of mimicking the C2PA read/write process of a
+    // typical signing flow.
+    c.bench_function("woff1_c2pa_readwrite_mimic", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(get_woff1_font_data());
+            let mut dest_stream = std::io::Cursor::new(Vec::new());
+
+            // Simulate the removal of the C2PA record
+            parse_font_and_execute_write(&mut font_stream, Some(&mut dest_stream), |font| {
+                if font.has_c2pa() {
+                   font.remove_c2pa_record()
+                       .expect("Failed to remove C2PA record");
+                }
+                let update_record = c2pa_font_handler::c2pa::UpdateContentCredentialRecord::builder()
+                    .with_active_manifest_uri("urn:monotype:example".to_string())
+                    .build();
+                font.update_c2pa_record(update_record)
+                    .expect("Failed to update C2PA record");
+            });
+
+            // Get the chunk positions, done by the SDK to determine general box
+            // positions
+            font_stream.set_position(0);
+            let _ = Woff1Font::get_chunk_positions(&mut font_stream)
+                .expect("Failed to get chunk positions");
+
+            // Simulate adding new C2PA record
+            let mut dest_stream = std::io::Cursor::new(Vec::new());
+            font_stream.set_position(0);
+            parse_font_and_execute_write(&mut font_stream, Some(&mut dest_stream), |font| {
+                let record = c2pa_font_handler::c2pa::UpdateContentCredentialRecord::builder()
+                    .with_active_manifest_uri("urn:monotype:example".to_string())
+                    .build();
+                font.update_c2pa_record(record)
+                    .expect("Failed to add C2PA record");
+            });
+
+            // Simulate reading C2PA for validation
+            let mut font_stream =
+                std::io::Cursor::new(dest_stream.get_ref().as_slice());
+            parse_font_and_execute(&mut font_stream, |font| {
+                font.get_c2pa().expect("Failed to read C2PA record");
+            });
+        });
+    });
+    */
+}
+
+/// Collection of benchmarks for WOFF1 font header reading and writing.
+fn woff1_header_benchmarks(c: &mut Criterion) {
+    c.bench_function("woff1_header_read", |b| {
+        b.iter(|| {
+            let header_data = get_woff1_header_data();
+            let mut font_stream = std::io::Cursor::new(header_data);
+            let _ = Woff1Header::from_reader_exact(
+                &mut font_stream,
+                0,
+                header_data.len(),
+            )
+            .expect("Failed to read WOFF1 font data");
+        });
+    });
+    c.bench_function("woff1_header_write", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(Vec::new());
+            let header = get_woff1_header();
+            header
+                .write(&mut font_stream)
+                .expect("Failed to write WOFF1 header");
+        });
+    });
+}
+
+/// Collection of benchmarks for WOFF1 font directory data reading and writing.
+fn woff1_directory_benchmarks(c: &mut Criterion) {
+    c.bench_function("woff1_directory_read", |b| {
+        b.iter(|| {
+            let data = get_woff1_directory_data();
+            let mut font_stream = std::io::Cursor::new(data);
+            let _ = Woff1Directory::from_reader_exact(
+                &mut font_stream,
+                0,
+                size_of::<Woff1DirectoryEntry>() * FONT_WOFF1_DIRECTORY_ENTRIES,
+            )
+            .expect("Failed to read WOFF1 font data");
+        });
+    });
+    c.bench_function("woff1_directory_write", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(Vec::new());
+            let directory = get_woff1_directory();
+            directory
+                .write(&mut font_stream)
+                .expect("Failed to write WOFF1 directory");
+        });
+    });
+}
+
+/// Collection of benchmarks for WOFF1 table reading and writing.
+fn woff1_table_benchmarks(c: &mut Criterion) {
+    c.bench_function("woff1_table_read", |b| {
+        b.iter(|| {
+            let (data, length) = get_faux_c2pa_table_data();
+            let mut font_stream = std::io::Cursor::new(data);
+            let _ = TableC2PA::from_reader_exact(&mut font_stream, 0, length)
+                .expect("Failed to read WOFF1 font data");
+        });
+    });
+    c.bench_function("woff1_table_write", |b| {
+        b.iter(|| {
+            let mut font_stream = std::io::Cursor::new(Vec::new());
+            let table = get_faux_c2pa_table();
+            table
+                .write(&mut font_stream)
+                .expect("Failed to write WOFF1 table");
+        });
+    });
+}
+
+criterion_group!(
+    name = benches;
+    config = Criterion::default().with_profiler(DhatProfiler::new());
+    targets =  woff1_table_benchmarks, woff1_directory_benchmarks, woff1_header_benchmarks, woff1_font_benchmarks,
+);
+criterion_main!(benches);
diff --git a/c2pa-font-handler/src/sfnt/table/c2pa_test.rs b/c2pa-font-handler/src/sfnt/table/c2pa_test.rs
index f1ab5b0..e24482c 100644
--- a/c2pa-font-handler/src/sfnt/table/c2pa_test.rs
+++ b/c2pa-font-handler/src/sfnt/table/c2pa_test.rs
@@ -610,3 +610,43 @@ fn test_table_c2pa_read_exact_with_invalid_uri_bytes() {
     let error = result.err().unwrap();
     assert!(matches!(error, FontIoError::StringFromUtf8(_)));
 }
+
+// Used to generate a test file for the C2PA table in the format of a font table
+#[ignore]
+#[test]
+fn test_table_c2pa_create() {
+    let mut table = TableC2PA {
+        active_manifest_uri: Some(
+            "https://example.com/example/font/cc.c2pa".to_string(),
+        ),
+        ..Default::default()
+    };
+    // Make a fake manifest store that is 2048 bytes long
+    let rng_gen = |seed: &mut u32| {
+        const A: u32 = 48271;
+        const M: u32 = 2147483647;
+        *seed = seed.wrapping_mul(A).wrapping_add(M);
+        (*seed >> 16) as u8
+    };
+
+    // Setup to do different bands so any compression will be
+    // beneficial
+    let bands = 14;
+    // Setup for ~14KB of data
+    let size = 1024 * bands;
+    let mut manifest_store = vec![0; size];
+    // Fill the manifest store with some random data
+    for i in 0..bands {
+        let mut seed: u32 = i as u32;
+        let value = rng_gen(&mut seed);
+        for j in 0..(size / bands) {
+            // Generate some random data
+            manifest_store[i * (size / bands) + j] = value;
+        }
+    }
+    table.manifest_store = Some(manifest_store);
+    let output_file = std::fs::File::create("faux_c2pa_table.bin").unwrap();
+    let mut writer = std::io::BufWriter::new(output_file);
+    let result = table.write(&mut writer);
+    assert!(result.is_ok());
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds benchmark suites for SFNT and WOFF1 font processing along with profiling support using the dhat heap profiler and corresponding documentation updates.  
- Added comprehensive benchmark tests for SFNT and WOFF1 font reading and writing routines.  
- Integrated dhat-based memory profiling into the benchmark configuration and updated CONTRIBUTING.md for guidance.  
- Updated Cargo.toml and spell-check configurations to support the new tooling.

### Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| c2pa-font-handler/src/sfnt/table/c2pa_test.rs | Added a test to generate a faux C2PA table for SFNT fonts. |
| c2pa-font-handler/benches/woff1.rs | Added benchmarks for WOFF1 font parsing, header, directory, and table operations. |
| c2pa-font-handler/benches/sfnt.rs | Added benchmarks for SFNT font parsing and C2PA record manipulation. |
| benches/utils/* | Added supporting utility modules for WOFF1 and SFNT benchmarks and profiling. |
| Cargo.toml & CONTRIBUTING.md | Updated dependencies and documentation to include criterion, dhat profiling, and benchmark instructions. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**c2pa-font-handler/benches/woff1.rs:142**
* The usage of size_of here may lead to a compilation error if 'std::mem::size_of' is not explicitly imported. Consider adding 'use std::mem::size_of;' at the top of the file.
```
size_of::<Woff1DirectoryEntry>() * FONT_WOFF1_DIRECTORY_ENTRIES,
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-14T14:55:30Z
user: login: brogdonm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T14:57:00Z
user: login: vancejc-mt
body: Works as designed, trying to reason out the flamegraph that's generated from the benchs and I'm not sure.. where I'd even start to poke to try and get sizing down.  But the data is there, and seemingly pretty consistent on my end (did 3 runs, each flamegraph looks the same).

Good spread of benches ✅ 
state: DISMISSED
author_association: NONE
submitted_at: 2025-05-14T20:12:47Z
user: login: vancejc-mt
body: Good to go 🚢 
state: APPROVED
author_association: NONE
submitted_at: 2025-05-14T20:23:05Z

Review comments:
diff_hunk: @@ -610,3 +610,43 @@ fn test_table_c2pa_read_exact_with_invalid_uri_bytes() {
     let error = result.err().unwrap();
     assert!(matches!(error, FontIoError::StringFromUtf8(_)));
 }
+
+// Used to generate a test file for the C2PA table in the format of a font table
+#[ignore]
+#[test]
+fn test_table_c2pa_create() {
+    let mut table = TableC2PA {
+        active_manifest_uri: Some(
+            "https://example.com/example/font/cc.c2pa".to_string(),
+        ),
+        ..Default::default()
+    };
+    // Make a fake manifest store that is 2048 bytes long
+    let rng_gen = |seed: &mut u32| {
+        const A: u32 = 48271;
+        const M: u32 = 2147483647;
+        *seed = seed.wrapping_mul(A).wrapping_add(M);
+        (*seed >> 16) as u8
+    };
+
+    // Setup to do different bands so any compression will be
+    // beneficial
+    let bands = 14;
+    // Setup for ~14KB of data
+    let size = 1024 * bands;
+    let mut manifest_store = vec![0; size];
+    // Fill the manifest store with some random data
+    for i in 0..bands {
+        let mut seed: u32 = i as u32;
+        let value = rng_gen(&mut seed);
+        for j in 0..(size / bands) {
+            // Generate some random data
+            manifest_store[i * (size / bands) + j] = value;
path: c2pa-font-handler/src/sfnt/table/c2pa_test.rs
user: login: Copilot
body: [nitpick] The RNG implementation in this loop only generates a single random value per band, resulting in each band being filled with identical data. Consider enhancing the randomness by generating a new value for each element to better simulate varied input if that is intended.
```suggestion
        for j in 0..(size / bands) {
            // Generate some random data
            manifest_store[i * (size / bands) + j] = rng_gen(&mut seed);
```
created_at: 2025-05-14T14:55:29Z
author_association: NONE
diff_hunk: @@ -610,3 +610,43 @@ fn test_table_c2pa_read_exact_with_invalid_uri_bytes() {
     let error = result.err().unwrap();
     assert!(matches!(error, FontIoError::StringFromUtf8(_)));
 }
+
+// Used to generate a test file for the C2PA table in the format of a font table
+#[ignore]
+#[test]
+fn test_table_c2pa_create() {
+    let mut table = TableC2PA {
+        active_manifest_uri: Some(
+            "https://example.com/example/font/cc.c2pa".to_string(),
+        ),
+        ..Default::default()
+    };
+    // Make a fake manifest store that is 2048 bytes long
+    let rng_gen = |seed: &mut u32| {
+        const A: u32 = 48271;
+        const M: u32 = 2147483647;
+        *seed = seed.wrapping_mul(A).wrapping_add(M);
+        (*seed >> 16) as u8
+    };
+
+    // Setup to do different bands so any compression will be
+    // beneficial
+    let bands = 14;
+    // Setup for ~14KB of data
+    let size = 1024 * bands;
+    let mut manifest_store = vec![0; size];
+    // Fill the manifest store with some random data
+    for i in 0..bands {
+        let mut seed: u32 = i as u32;
+        let value = rng_gen(&mut seed);
+        for j in 0..(size / bands) {
+            // Generate some random data
+            manifest_store[i * (size / bands) + j] = value;
path: c2pa-font-handler/src/sfnt/table/c2pa_test.rs
user: login: brogdonm
body: This was done on purpose to have data that would compress good for woff testing.
created_at: 2025-05-14T14:57:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,6 +121,41 @@ should include with:
 mod tests;
 ```
 
+## Benchmarking
+
+This project uses `criterion` as the benchmarking framework. Criterion provides statistically sound performance measurements, including throughput estimates and confidence intervals, and works on stable Rust.
+
+To run benchmarks:
+
+```bash
+cargo bench
+```
+
+This will execute any benchmarks defined in the [c2pa-font-handler/Cargo.toml](./c2pa-font-handler/Cargo.toml) file and output performance summaries to the console. Criterion also generates detailed reports, including time distributions and trend analysis, stored in the `target/criterion` directory.
+
+> Note on Accuracy: Benchmark results can vary depending on current system load, CPU frequency scaling, and background tasks. For more stable and repeatable outcomes, run benchmarks on an idle system with consistent CPU performance settings (e.g., disabling turbo boost or using `taskset`/`cpuset` on Linux to isolate cores).
+
+### Profiling
+
+For memory profiling of benchmarks, this project supports integration with the `dhat` heap profiler. This allows tracking allocations during benchmarking runs.
+
+To profile, the only difference is how the benches are ran. For example the following will
+perform profiling of the WOFF1 benchmarks:
+
+```bash
+cargo test --profile release --bench woff1 -- --profile-time 3 --bench
path: CONTRIBUTING.md
user: login: vancejc-mt
body: I had to do `cargo test --profile release --bench woff1 --features=woff -- --profile-time 3 --bench` (since it requires `woff`) but.. might just be something I set up wrong?
created_at: 2025-05-14T20:00:49Z
author_association: NONE

PR comments:
user: login: brogdonm
created_at: 2025-05-14T20:21:25Z
body: > Works as designed, trying to reason out the flamegraph that's generated from the benchs and I'm not sure.. where I'd even start to poke to try and get sizing down. But the data is there, and seemingly pretty consistent on my end (did 3 runs, each flamegraph looks the same).
> 
> Good spread of benches ✅

I fixed up the documentation to have the blurb about `--features="woff"` to run for WOFF.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-14T20:23:22Z
body: <h2>Coverage using rust version: stable</h2>Coverage after merging <b>feature/C2PA-678/benchmarks</b> into <b>dev</b> will be<table><tbody><tr><th>99.67%</th></tr></tbody></table>

<details><summary>Coverage Report for Changed Files</summary><table><tbody><tr><th>File</th><th>Stmts</th><th>Branches</th><th>Funcs</th><th>Lines</th><th>Uncovered Lines</th></tr></tbody></table></details>
author_association: CONTRIBUTOR