chore: import upstream snapshot with attribution
CI / lint (3.11) (push) Has been cancelled
CI / lint (3.12) (push) Has been cancelled
CI / lint (3.13) (push) Has been cancelled
CI / shellcheck (push) Has been cancelled
CI / shfmt (push) Has been cancelled
CI / setup (3.11) (push) Has been cancelled
CI / setup (3.12) (push) Has been cancelled
CI / setup (3.13) (push) Has been cancelled
CI / check-licenses (3.12) (push) Has been cancelled
CI / test_unit (3.11) (push) Has been cancelled
CI / test_unit (3.12) (push) Has been cancelled
CI / test_unit (3.13) (push) Has been cancelled
CI / test_unit_no_extras (3.11) (push) Has been cancelled
CI / test_unit_no_extras (3.12) (push) Has been cancelled
CI / test_json_to_html (3.12) (push) Has been cancelled
CI / test_unit_no_extras (3.13) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.12, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.11, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.12, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.11, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.13, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.11, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.12, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.13, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.11, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.12, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.13, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.11, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.12, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.13, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.11, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.12, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.13, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.11, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.12, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.13, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.11, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.12, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.13, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
Build And Push Docker Image / set-short-sha (push) Has been cancelled
Partition Benchmark / setup (push) Has been cancelled
Partition Benchmark / Measure and compare partition() runtime (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.13, --extra xlsx) (push) Has been cancelled
CI / test_ingest_src (3.12) (push) Has been cancelled
CI / test_json_to_markdown (3.12) (push) Has been cancelled
CI / changelog (push) Has been cancelled
CI / test_dockerfile (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/amd64, opensource-linux-8core) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build And Push Docker Image / publish-images (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:33:56 +08:00
commit 461bf6fd40
1313 changed files with 1079898 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
benchmark_results
profile_results
+45
View File
@@ -0,0 +1,45 @@
# Performance
This is a collection of tools helpful for inspecting and tracking performance of the Unstructured library.
The benchmarking script allows a user to track performance time to partitioning results against a fixed set of test documents and store those results with indication of architecture, instance type, and git hash, in S3.
The profiling script allows a user to inspect how time time and memory are spent across called functions when performing partitioning on a given document.
## Install
Benchmarking requires no additional dependencies and should work without any initial setup.
Profiling has a few dependencies which can be installed with:
```bash
pip install -r scripts/performance/requirements.txt
npm install -g speedscope
```
The second dependency `speedscope` provides a tool to view profiling results from `py-spy` locally. Alternatively you can also drop the profile result `*.speedscope` into https://www.speedscope.app/ to view the results online.
## Run
### Benchmark
Export / assign desired environment variable settings:
- DOCKER_TEST: Set to true to run benchmark inside a Docker container (default: false)
- NUM_ITERATIONS: Number of iterations for benchmark (e.g., 100) (default: 3)
- INSTANCE_TYPE: Type of benchmark instance (e.g., "c5.xlarge") (default: unspecified)
- PUBLISH_RESULTS: Set to true to publish results to S3 bucket (default: false)
-
Usage: `./scripts/performance/benchmark.sh`
### Profile
Export / assign desired environment variable settings:
- DOCKER_TEST: Set to true to run profiling inside a Docker container (default: false)
Usage:
**on Linux**: `./scripts/performance/profile.sh`
**on macOS**: `sudo -E ./scripts/performance/profile.sh`; `py-spy` requires su to run on macOS
- Run the script and choose the profiling mode: 'run' or 'view'.
- In the 'run' mode, you can profile custom files or select existing test files.
- In the 'view' mode, you can view previously generated profiling results.
- The script supports time profiling with cProfile and memory profiling with memray.
- Users can choose different visualization options such as flamegraphs, tables, trees, summaries, and statistics.
- Test documents are synced from an S3 bucket to a local directory before running the profiles
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# This is intended solely to be called by scripts/performance/benchmark.sh.
# This file is separated out to allow us to easily execute this part of the test script inside a Docker container.
SCRIPT_DIR=$(dirname "$0")
TEST_DOCS_FOLDER="$SCRIPT_DIR/docs"
TIMEFORMAT="%R"
mkdir -p "$SCRIPT_DIR/benchmark_results" >/dev/null 2>&1
DATE=$(date +"%Y-%m-%d_%H-%M-%S")
RESULTS_FILE="$SCRIPT_DIR/benchmark_results/${DATE}_benchmark_results_${INSTANCE_TYPE}_$("$SCRIPT_DIR/get-stats-name.sh")_$GIT_HASH.csv"
echo "Test File,Iterations,Average Execution Time (s)" >"$RESULTS_FILE"
echo "Starting benchmark test..."
for file in "$TEST_DOCS_FOLDER"/*; do
echo "Testing file: $(basename "$file")"
if [[ " ${SLOW_FILES[*]} " =~ $(basename "$file") ]]; then
echo "File found in slow files list. Running once..."
num_iterations=1
else
# shellcheck disable=SC2153
num_iterations=$NUM_ITERATIONS
fi
strategy="fast"
if [[ " ${HI_RES_STRATEGY_FILES[*]} " =~ $(basename "$file") ]]; then
echo "Testing with hi_res strategy"
strategy="hi_res"
fi
if ! response=$(python3 -m "scripts.performance.time_partition" "$file" "$num_iterations" "$strategy"); then
echo "error: $response"
exit 1
fi
average_time=$(echo "$response" | awk '/Average time:/ {print $3}')
echo "Average execution time: $average_time seconds"
echo "$(basename "$file"),$num_iterations,$average_time" >>"$RESULTS_FILE"
done
# NOTE: Be careful if updating this message. The benchmarking script looks for this message to get the CSV file name.
echo "Benchmarking completed. Results saved to: $(basename "$RESULTS_FILE")"
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Usage:
# - Set the required environment variables (listed below)
# - Run the script: ./scripts/performance/benchmark.sh
# Environment Variables:
# - DOCKER_TEST: Set to "true" to run benchmark inside a Docker container (default: false)
# - NUM_ITERATIONS: Number of iterations for benchmark (e.g., 100) (default: 3)
# - INSTANCE_TYPE: Type of benchmark instance (e.g., "c5.xlarge") (default: "unspecified")
# - PUBLISH_RESULTS: Set to "true" to publish results to S3 bucket (default: false)
SLOW_FILES=("DA-619p.pdf" "layout-parser-paper-hi_res-16p.pdf" "layout-parser-paper-10p.jpg")
HI_RES_STRATEGY_FILES=("layout-parser-paper-hi_res-16p.pdf")
NUM_ITERATIONS=${NUM_ITERATIONS:-2}
INSTANCE_TYPE=${INSTANCE_TYPE:-"unspecified"}
S3_BUCKET="utic-dev-tech-fixtures"
S3_RESULTS_DIR="performance-test/results"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GIT_HASH="$(git rev-parse --short HEAD)"
# Save the results filename to a temporary file
RESULTS_FILENAME_FILE=$(mktemp)
trap 'rm -f $RESULTS_FILENAME_FILE' EXIT
function read_benchmark_logs_for_results() {
if [[ $line =~ Results\ saved\ to:\ ([^\ ]+) ]]; then
results_filename="${BASH_REMATCH[1]}"
echo "CSV file value found: $results_filename"
echo "$results_filename" >"$RESULTS_FILENAME_FILE" # Store the value in the temporary file
fi
}
if [[ "$DOCKER_TEST" == "true" ]]; then
DOCKER_IMAGE=unstructured:perf-test make docker-build
docker rm -f unstructured-perf-test >/dev/null 2>&1
docker run \
--name unstructured-perf-test \
--rm \
-e NUM_ITERATIONS="$NUM_ITERATIONS" \
-e INSTANCE_TYPE="$INSTANCE_TYPE" \
-e GIT_HASH="$GIT_HASH" \
-e SLOW_FILES="${SLOW_FILES[*]}" \
-e HI_RES_STRATEGY_FILES="${HI_RES_STRATEGY_FILES[*]}" \
-v "${SCRIPT_DIR}":/home/notebook-user/scripts/performance \
unstructured:perf-test \
bash /home/notebook-user/scripts/performance/benchmark-local.sh 2>&1 | tee >(while IFS= read -r line; do
read_benchmark_logs_for_results
done)
else
NUM_ITERATIONS="$NUM_ITERATIONS" INSTANCE_TYPE="$INSTANCE_TYPE" GIT_HASH="$GIT_HASH" SLOW_FILES="${SLOW_FILES[*]}" HI_RES_STRATEGY_FILES="${HI_RES_STRATEGY_FILES[*]}" "$SCRIPT_DIR"/benchmark-local.sh 2>&1 |
tee >(while IFS= read -r line; do
read_benchmark_logs_for_results
done)
fi
# Read the result filename from the temporary file
results_filename=$(<"$RESULTS_FILENAME_FILE")
if [[ -z $results_filename ]]; then
echo "Error: Results filename value not found in the benchmark logs."
exit 1
fi
if [[ "$PUBLISH_RESULTS" == "true" ]]; then
S3_RESULTS_PATH="$S3_BUCKET/$S3_RESULTS_DIR"
echo "Publishing results to S3 bucket: $S3_RESULTS_PATH"
aws s3 cp "$SCRIPT_DIR/benchmark_results/$results_filename" "s3://$S3_RESULTS_PATH/"
fi
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Measure partition() runtime over a fixed set of representative example-docs files.
Follows the same conventions as the existing scripts/performance tooling:
- PDFs and images are run with strategy="hi_res".
- Everything else is run with strategy="fast".
- Each file is timed over NUM_ITERATIONS runs (after a warmup) and the
average is recorded, matching time_partition.py behaviour.
Writes a JSON file mapping each file to its average runtime, plus a ``__total__``
key with the wall-clock total. An optional positional argument sets the output
path (default: scripts/performance/partition-speed-test/benchmark_results.json).
Also writes the total duration to $GITHUB_OUTPUT as ``duration=<seconds>``.
Usage:
uv run --no-sync python scripts/performance/benchmark_partition.py [output.json]
Environment variables:
NUM_ITERATIONS number of timed iterations per file (default: 1)
"""
from __future__ import annotations
import json
import logging
import os
import sys
import time
from pathlib import Path
from unstructured.partition.auto import partition
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
BENCHMARK_FILES: list[tuple[str, str]] = [
# PDFs - hi_res
("example-docs/pdf/a1977-backus-p21.pdf", "hi_res"),
("example-docs/pdf/copy-protected.pdf", "hi_res"),
("example-docs/pdf/reliance.pdf", "hi_res"),
("example-docs/pdf/pdf-with-ocr-text.pdf", "hi_res"),
# Images - hi_res
("example-docs/double-column-A.jpg", "hi_res"),
("example-docs/double-column-B.jpg", "hi_res"),
("example-docs/embedded-images-tables.jpg", "hi_res"),
# Other document types - fast
("example-docs/contains-pictures.docx", "fast"),
("example-docs/example-10k-1p.html", "fast"),
("example-docs/science-exploration-1p.pptx", "fast"),
]
NUM_ITERATIONS: int = int(os.environ.get("NUM_ITERATIONS", "1"))
DEFAULT_OUTPUT = Path(__file__).parent / "partition-speed-test" / "benchmark_results.json"
def _warmup(filepath: str) -> None:
"""Run a single fast-strategy partition to warm the process up.
Mirrors warm_up_process() in time_partition.py: uses a warmup-docs/
variant if present, otherwise falls back to the file itself.
"""
warmup_dir = Path(__file__).parent / "warmup-docs"
warmup_file = warmup_dir / f"warmup{Path(filepath).suffix}"
target = str(warmup_file) if warmup_file.exists() else filepath
partition(target, strategy="fast")
def _measure(filepath: str, strategy: str, iterations: int) -> float:
"""Return the average wall-clock seconds for partitioning *filepath*.
Identical logic to time_partition.measure_execution_time().
"""
total = 0.0
for _ in range(iterations):
t0 = time.perf_counter()
partition(filepath, strategy=strategy)
total += time.perf_counter() - t0
return total / iterations
def _set_github_output(key: str, value: str) -> None:
"""Write key=value to $GITHUB_OUTPUT when running in Actions."""
gho = os.environ.get("GITHUB_OUTPUT")
if gho:
with open(gho, "a") as fh:
fh.write(f"{key}={value}\n")
def main() -> None:
output_path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_OUTPUT
repo_root = Path(__file__).resolve().parent.parent.parent # scripts/performance/ -> repo root
logger.info("=" * 60)
logger.info(f"Partition benchmark (NUM_ITERATIONS={NUM_ITERATIONS})")
logger.info("=" * 60)
results: dict[str, float] = {}
grand_start = time.perf_counter()
for rel_path, strategy in BENCHMARK_FILES:
filepath = repo_root / rel_path
if not filepath.exists():
logger.warning(f" WARNING: {rel_path} not found skipping.")
continue
logger.info(f" {rel_path} (strategy={strategy}, iterations={NUM_ITERATIONS})")
_warmup(str(filepath))
avg = _measure(str(filepath), strategy, NUM_ITERATIONS)
results[rel_path] = round(avg, 4)
logger.info(f" avg {avg:.2f}s")
total_seconds = round(time.perf_counter() - grand_start, 2)
results["__total__"] = total_seconds
logger.info(f"\nTotal wall-clock time: {total_seconds}s")
# Write JSON results file (consumed by compare_benchmark.py)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(results, indent=2) + "\n")
logger.info(f"Results written to {output_path}")
# Also expose total as a GitHub Actions step output
_set_github_output("duration", str(int(total_seconds)))
if __name__ == "__main__":
main()
+284
View File
@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""Compare this run's partition() benchmark against a rolling baseline.
Why a rolling baseline (and not a single stored "best"):
GitHub's shared ``ubuntu-latest`` runners vary in speed by ~1.6x run-to-run,
and that variance scales the whole benchmark (partition work *and* fixed
overhead). A single all-time-minimum baseline is therefore unbeatable the
moment one lucky-fast runner records it -- every later run on a normal runner
exceeds best*(1+threshold) and the check fails for everyone. (That is exactly
what happened: a frozen 81s "best" vs a real ~130s fleet.)
Instead we keep the last ``--window`` runs from ``main`` and compare against
their **median**, which a single fast/slow outlier can't poison. The baseline
tracks reality over time rather than ratcheting to an unrepeatable minimum.
Storage:
Each ``main`` run is one immutable JSON record under ``HISTORY_DIR`` (synced
to/from S3 by the workflow). This script never talks to S3 -- it only reads
the local history dir and, with ``--record``, writes this run's record there
for the workflow to upload. Pruning of old objects is an S3 lifecycle rule.
Usage:
uv run --no-sync python scripts/performance/compare_benchmark.py \
benchmark_results.json \
HISTORY_DIR \
[--threshold 0.30] [--window 20] [--min-samples 5] [--record]
benchmark_results.json this run's results from benchmark_partition.py
HISTORY_DIR dir of per-run history records (may be empty/missing)
--threshold regression allowance over the median (default 0.30)
--window number of most-recent records to median over (default 20)
--min-samples below this many records, warm-up (record + pass, no gate)
--record write this run's record into HISTORY_DIR (main runs only);
recording happens regardless of pass/fail so the baseline
can't get stuck.
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import logging
import math
import os
import statistics
import sys
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
def _github_output(key: str, value: str) -> None:
"""Write a key=value pair to $GITHUB_OUTPUT when running in Actions."""
gho = os.environ.get("GITHUB_OUTPUT")
if gho:
with open(gho, "a") as fh:
fh.write(f"{key}={value}\n")
def _fmt(seconds: float) -> str:
if seconds is None or math.isnan(seconds):
return " n/a"
return f"{seconds:7.2f}s"
def _pct_diff(current: float, baseline: float) -> str:
if not baseline:
return " n/a"
diff = (current - baseline) / baseline * 100
sign = "+" if diff >= 0 else ""
return f"{sign}{diff:.1f}%"
def _runner_info() -> dict:
"""Best-effort CPU/nproc capture, purely for later visibility of variance."""
cpu_model = None
try:
for line in Path("/proc/cpuinfo").read_text().splitlines():
if line.lower().startswith("model name"):
cpu_model = line.split(":", 1)[1].strip()
break
except OSError:
pass
return {"cpu_model": cpu_model, "nproc": os.cpu_count()}
def _is_number(value: object) -> bool:
"""True for real numeric values; bool is rejected (it's a subclass of int)."""
return isinstance(value, (int, float)) and not isinstance(value, bool)
def load_history(history_dir: Path) -> list[dict]:
"""Load every per-run record from HISTORY_DIR, sorted oldest -> newest.
Records are deduped by sha (keeping the newest per sha by timestamp) so the
median is correct even if S3 still holds legacy timestamped objects that share
a sha. Records without a sha are kept individually -- they're never collapsed
together. Records whose ``total`` is missing or non-numeric are skipped so a
malformed object can't crash ``statistics.median``.
"""
if not history_dir.is_dir():
return []
records: list[dict] = []
for f in history_dir.glob("*.json"):
try:
rec = json.loads(f.read_text())
except (json.JSONDecodeError, OSError) as e:
logger.warning(f"skipping unreadable history record {f.name}: {e}")
continue
if isinstance(rec, dict) and _is_number(rec.get("total")):
records.append(rec)
else:
logger.warning(f"skipping history record {f.name}: missing/non-numeric 'total'")
records.sort(key=lambda r: r.get("timestamp") or "")
# Dedupe by sha, keeping the newest record per sha. Records sort oldest->newest
# above, so a later same-sha record overwrites the earlier one. Empty/absent sha
# is kept per-record (do not collapse all sha-less records together).
deduped: list[dict] = []
by_sha: dict[str, int] = {}
for rec in records:
sha = rec.get("sha") or ""
if sha and sha in by_sha:
deduped[by_sha[sha]] = rec
else:
if sha:
by_sha[sha] = len(deduped)
deduped.append(rec)
# Overwriting a same-sha record in place keeps the *first* occurrence's slot but
# the *newer* record's timestamp, which can leave deduped out of chronological
# order. Re-sort so callers can rely on history[-window:] being the newest runs.
return sorted(deduped, key=lambda r: r.get("timestamp") or "")
def build_record(current: dict) -> dict:
"""Build this run's history record from results + CI environment metadata."""
now = dt.datetime.now(dt.timezone.utc)
sha = os.environ.get("GITHUB_SHA", "")
return {
"sha": sha,
"run_id": int(os.environ["GITHUB_RUN_ID"]) if os.environ.get("GITHUB_RUN_ID") else None,
"timestamp": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
"event": os.environ.get("GITHUB_EVENT_NAME"),
"ref": os.environ.get("GITHUB_REF_NAME"),
"seed": False,
"iterations": int(os.environ.get("NUM_ITERATIONS", "0")) or None,
"runner": _runner_info(),
"total": round(current["__total__"], 2),
"per_file": {k: round(v, 4) for k, v in current.items() if k != "__total__"},
}
def write_record(history_dir: Path, record: dict) -> Path:
"""Write the record into HISTORY_DIR, replacing any prior record for this sha.
The object name is keyed by sha alone (not timestamped), so a re-run of the
same commit overwrites the same object. This is what keeps the rolling median
from double-counting a commit: the workflow's ``aws s3 sync`` (no ``--delete``)
would otherwise leave a stale, differently-named same-sha object behind. The
timestamp survives as a record field, which is what load/sort uses.
"""
history_dir.mkdir(parents=True, exist_ok=True)
sha = record.get("sha") or ""
if sha:
name = f"{sha[:12]}.json"
else:
# No sha (e.g. local/manual): fall back to a timestamp-derived name. Keep it
# filesystem-safe by stripping the separators, as the old naming did.
name = record["timestamp"].replace("-", "").replace(":", "") + ".json"
out = history_dir / name
out.write_text(json.dumps(record, indent=2) + "\n")
return out
def _median_per_file(window: list[dict]) -> dict[str, float]:
files: set[str] = set()
for r in window:
files.update(r.get("per_file", {}).keys())
medians: dict[str, float] = {}
for fname in files:
vals = [
r["per_file"][fname] for r in window if _is_number(r.get("per_file", {}).get(fname))
]
if vals:
medians[fname] = statistics.median(vals)
return medians
def main() -> None:
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
ap.add_argument("current_results", type=Path)
ap.add_argument("history_dir", type=Path)
ap.add_argument("--threshold", type=float, default=0.30)
ap.add_argument("--window", type=int, default=20)
ap.add_argument("--min-samples", type=int, default=5)
ap.add_argument(
"--record", action="store_true", help="write this run into the history (main runs)"
)
args = ap.parse_args()
# --window is used as history[-args.window:]; 0 silently means "all history"
# ([-0:] == [0:]) and negatives slice from the wrong end. Require >= 1.
if args.window < 1:
ap.error("--window must be >= 1")
if args.min_samples < 0:
ap.error("--min-samples must be >= 0")
current: dict[str, float] = json.loads(args.current_results.read_text())
current_total: float = current["__total__"]
history = load_history(args.history_dir)
window = history[-args.window :]
# Record first (main runs only) so the baseline always reflects reality,
# regardless of whether this run passes the gate -- the opposite of the old
# min-ratchet, which could only ever lower the bar and so got stuck.
if args.record:
rec_path = write_record(args.history_dir, build_record(current))
logger.info(f"recorded this run -> {rec_path.name}")
# Warm-up: not enough history to gate yet. Observe and pass. An empty window can
# never produce a baseline (statistics.median([]) raises), so it is always warm-up
# regardless of --min-samples (which may be 0).
if not window or len(window) < args.min_samples:
logger.info(
f"WARM-UP: {len(window)} of {args.min_samples} baseline samples present "
f"-- recording only, not gating. (current total {current_total:.2f}s)"
)
_github_output("regression", "false")
_github_output("warmup", "true")
sys.exit(0)
baseline = statistics.median(r["total"] for r in window)
limit = baseline * (1.0 + args.threshold)
per_file_baseline = _median_per_file(window)
all_files = sorted((set(current) | set(per_file_baseline)) - {"__total__"})
col_w = max((len(f) for f in all_files), default=40) + 2
header = f"{'File':<{col_w}} {'Current':>9} {'Median':>9} {'Delta':>8}"
logger.info("=" * len(header))
logger.info(f"Partition benchmark vs rolling median of last {len(window)} main runs")
logger.info("=" * len(header))
logger.info(header)
logger.info("-" * len(header))
for fname in all_files:
c = current.get(fname, float("nan"))
b = per_file_baseline.get(fname, float("nan"))
logger.info(f"{fname:<{col_w}} {_fmt(c)} {_fmt(b)} {_pct_diff(c, b):>8}")
logger.info("-" * len(header))
logger.info(
f"{'TOTAL':<{col_w}} {_fmt(current_total)} {_fmt(baseline)}"
f" {_pct_diff(current_total, baseline):>8}"
)
logger.info("")
logger.info(
f"Baseline : median of {len(window)} runs = {baseline:.2f}s "
f"(threshold {args.threshold * 100:.0f}%, fail if > {limit:.2f}s)"
)
logger.info("")
if current_total > limit:
excess_pct = (current_total - baseline) / baseline * 100
logger.error(
f"FAIL: current runtime {current_total:.2f}s exceeds the median baseline "
f"{baseline:.2f}s by {excess_pct:.1f}% (threshold {args.threshold * 100:.0f}%, "
f"limit {limit:.2f}s)."
)
_github_output("regression", "true")
sys.exit(1)
logger.info(
f"PASS: {current_total:.2f}s is within {args.threshold * 100:.0f}% of the "
f"median baseline {baseline:.2f}s."
)
_github_output("regression", "false")
sys.exit(0)
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
../../../example-docs/DA-1p.pdf
+1
View File
@@ -0,0 +1 @@
../../../example-docs/DA-619p.pdf
@@ -0,0 +1 @@
../../../example-docs/book-war-and-peace-1225p.txt
+1
View File
@@ -0,0 +1 @@
../../../example-docs/book-war-and-peace-1p.txt
+1
View File
@@ -0,0 +1 @@
../../../example-docs/example-10k-1p.html
+1
View File
@@ -0,0 +1 @@
../../../example-docs/example-10k-230p.html
+1
View File
@@ -0,0 +1 @@
../../../example-docs/handbook-1p.docx
+1
View File
@@ -0,0 +1 @@
../../../example-docs/handbook-872p.docx
+1
View File
@@ -0,0 +1 @@
../../../example-docs/layout-parser-paper-10p.jpg
+1
View File
@@ -0,0 +1 @@
../../../example-docs/layout-parser-paper-fast.jpg
@@ -0,0 +1 @@
../../../example-docs/layout-parser-paper.pdf
@@ -0,0 +1 @@
../../../example-docs/layout-parser-paper.pdf
+1
View File
@@ -0,0 +1 @@
../../../example-docs/science-exploration-1p.pptx
@@ -0,0 +1 @@
../../../example-docs/science-exploration-369p.pptx
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# get a string representing the system stats. we should be able to infer
# this from aws types, but this guarantees we have the info we need in all cases
# hack to get gpus available for processing
# assumes nvidia drivers available for inference tasks
if command -v nvidia-smi &>/dev/null; then
gpu=$(nvidia-smi --query-gpu=name --format=csv,noheader | wc -l)
else
gpu="0"
fi
if command -v sysctl >/dev/null && command -v system_profiler >/dev/null; then
cpu=$(sysctl -n hw.logicalcpu_max)
mem=$(sysctl -n hw.memsize | awk '{printf "%.0fGB",$0/1024/1024/1024}')
else
cpu=$(getconf _NPROCESSORS_ONLN)
mem=$(grep 'MemTotal' /proc/meminfo | awk '{printf "%.0fGB",$2/1024/1024}')
fi
echo "${cpu}cpu_${gpu}gpu_${mem}mem"
+365
View File
@@ -0,0 +1,365 @@
#!/usr/bin/env bash
# Performance profiling and visualization of code using cProfile and memray.
# Environment Variables:
# - DOCKER_TEST: Set to true to run profiling inside a Docker container (default: false)
# Usage:
# - Run the script and choose the profiling mode: 'run' or 'view'.
# - In the 'run' mode, you can profile custom files or select existing test files.
# - In the 'view' mode, you can view previously generated profiling results.
# - The script supports time profiling with cProfile and memory profiling with memray.
# - Users can choose different visualization options such as flamegraphs, tables, trees, summaries, and statistics.
# - Test documents are (optionally) synced from an S3 bucket to a local directory before running the profiles.
# Dependencies:
# - memray package for memory profiling and visualization.
# - flameprof and snakeviz for time profiling and visualization.
# - AWS CLI for syncing files from S3 (if applicable).
# Package dependencies can be installed with `uv pip install -r scripts/performance/requirements.txt`
# Usage example:
# ./scripts/performance/profile.sh
# NOTE: because memray does not build wheels for ARM-Linux, this script can not run in an ARM Docker container on an M1 Mac (though emulated AMD would work).
# Validate dependencies
check_python_module() {
if ! python3 -c "import $1" >/dev/null 2>&1; then
echo "Error: Python module $1 is not installed. Please install required dependencies with 'uv pip install -r scripts/performance/requirements.txt'."
exit 1
fi
}
validate_dependencies() {
check_python_module memray
check_python_module flameprof
}
# only validate in non-docker context (since we install dependencies on the fly in docker)
if [[ "$DOCKER_TEST" != "true" ]]; then
validate_dependencies
fi
SCRIPT_DIR=$(dirname "$0")
# Convert the relative path to module notation
MODULE_PATH=${SCRIPT_DIR////.}
# Remove the leading dot if it exists
MODULE_PATH=${MODULE_PATH#.}
# Remove the leading dot if it exists again
MODULE_PATH=${MODULE_PATH#\.}
PROFILE_RESULTS_DIR="$SCRIPT_DIR/profile_results"
# Create PROFILE_RESULTS_DIR if it doesn't exist
mkdir -p "$PROFILE_RESULTS_DIR"
if [[ "$DOCKER_TEST" == "true" ]]; then
SCRIPT_PARENT_DIR=$(dirname "$(dirname "$(realpath "$0")")")
docker run -it --rm -v "$SCRIPT_PARENT_DIR:/home/unstructured/scripts" unstructured:dev /bin/bash -c "
cd unstructured/
uv pip install -r scripts/performance/requirements.txt
echo \"Warming the Docker container by running a small partitioning job..\"
python3 -c 'from unstructured.partition.auto import partition; partition(\"'""$SCRIPT_DIR/warmup_docs/warmup.pdf'\", strategy=\"hi_res\")[1]'
./scripts/performance/profile.sh
"
exit 0
fi
check_display() {
if system_profiler SPDisplaysDataType 2>/dev/null | grep -q "Display Type"; then
return 0 # Display is present
else
return 1 # Display is not present (headless context)
fi
}
view_profile_headless() {
# Several of the visualization options require a graphical interface. If DISPLAY is not set, we can't use those options.
extension=".bin"
result_file="${result_file%.*}$extension"
if [[ ! -f "$result_file" ]]; then
unset result_file # Unset the result_file variable to go back to the "Select a file" view
echo "Result file not found. Please choose a different profile type or go back."
else
while true; do
read -r -p "Choose visualization type: (1) tree (2) summary (3) stats (b) back, (q) quit: " -n 1 visualization_type
echo
if [[ $visualization_type == "b" ]]; then
unset result_file # Unset the result_file variable to go back to the "Select a file" view
break
elif [[ $visualization_type == "q" ]]; then
exit 0
fi
case $visualization_type in
"1")
python3 -m memray tree "$result_file"
;;
"2")
python3 -m memray summary "$result_file"
;;
"3")
python3 -m memray stats "$result_file"
;;
*)
echo "Invalid visualization type. Please try again."
;;
esac
done
fi
}
view_profile_with_head() {
while true; do
read -r -p "Choose profile type: (1) time (2) memory (3) speedscope (b) back, (q) quit: " -n 1 profile_type
echo
if [[ $profile_type == "b" ]]; then
unset result_file # Unset the result_file variable to go back to the "Select a file" view
break
elif [[ $profile_type == "q" ]]; then
exit 0
fi
if [[ $profile_type == "1" ]]; then
extension=".prof"
elif [[ $profile_type == "2" ]]; then
extension=".bin"
elif [[ $profile_type == "3" ]]; then
extension=".speedscope"
else
echo "Invalid profile type. Please try again."
continue
fi
result_file="${result_file%.*}$extension"
if [[ ! -f "$result_file" ]]; then
echo "Result file not found. Please choose a different profile type or go back."
continue
fi
if [[ $profile_type == "3" ]]; then
speedscope "$result_file"
elif [[ $profile_type == "2" ]]; then
while true; do
read -r -p "Choose visualization type: (1) flamegraph (2) table (3) tree (4) summary (5) stats (b) back, (q) quit: " -n 1 visualization_type
echo
if [[ $visualization_type == "b" ]]; then
break
elif [[ $visualization_type == "q" ]]; then
exit 0
fi
case $visualization_type in
"1")
rm -f "${result_file}.memray.html"
python3 -m memray flamegraph -o "${result_file}.memray.html" "$result_file"
open "${result_file}.memray.html"
;;
"2")
rm -f "${result_file}.table.html"
python3 -m memray table -o "${result_file}.table.html" "$result_file"
open "${result_file}.table.html"
;;
"3")
python3 -m memray tree "$result_file"
;;
"4")
python3 -m memray summary "$result_file"
;;
"5")
python3 -m memray stats "$result_file"
;;
*)
echo "Invalid visualization type. Please try again."
;;
esac
done
else
while true; do
read -r -p "Choose visualization type: (1) flamegraph (2) snakeviz (b) back, (q) quit: " -n 1 visualization_type
echo
if [[ $visualization_type == "b" ]]; then
break
elif [[ $visualization_type == "q" ]]; then
exit 0
fi
case $visualization_type in
"1")
flameprof_file="${result_file}.flameprof.svg"
rm -f "$flameprof_file"
python3 -m flameprof "$result_file" >"$flameprof_file"
open "$flameprof_file"
;;
"2")
snakeviz "$result_file"
;;
*)
echo "Invalid visualization type. Please try again."
;;
esac
done
fi
break # Return to the beginning
done
}
view_profile() {
if [ -n "$1" ]; then
result_file="$1"
fi
while true; do
if [[ -z $result_file ]]; then
echo "Available result files:"
result_files=("$PROFILE_RESULTS_DIR"/*.bin)
if [[ ${#result_files[@]} -eq 0 ]]; then
echo "No result files found."
return
fi
for ((i = 0; i < ${#result_files[@]}; i++)); do
filename="${result_files[$i]##*/}"
filename="${filename%.*}"
echo "$i. $filename"
done
read -r -p "Enter the number corresponding to the result file you want to view (b to go back, q to quit): " selection
if [[ $selection == "b" ]]; then
return
elif [[ $selection == "q" ]]; then
exit 0
fi
result_file="${result_files[$selection]}"
fi
if check_display; then
view_profile_with_head "$result_file"
else
view_profile_headless "$result_file"
fi
done
}
run_profile() {
while true; do
read -r -p "Choose an option: 1) Existing test file, (2) Custom file, (b) back, (q) quit: " -n 1 option
echo
if [[ $option == "b" ]]; then
return
elif [[ $option == "q" ]]; then
exit 0
fi
if [[ $option == "1" ]]; then
echo "Available test files:"
test_files=("$SCRIPT_DIR/docs"/*)
if [[ ${#test_files[@]} -eq 0 ]]; then
echo "No test files found."
return
fi
for ((i = 0; i < ${#test_files[@]}; i++)); do
echo "$i. ${test_files[$i]}"
done
read -r -p "Enter the number corresponding to the test file you want to run followed by return (b to go back, q to quit): " selection
if [[ $selection == "b" ]]; then
return
elif [[ $selection == "q" ]]; then
exit 0
fi
test_file="${test_files[$selection]}"
elif [[ $option == "2" ]]; then
read -r -p "Enter the path to the custom file: " test_file
else
echo "Invalid option. Please try again."
continue
fi
# Delete the output files if they exist
rm -f "$PROFILE_RESULTS_DIR/${test_file##*/}.prof"
rm -f "$PROFILE_RESULTS_DIR/${test_file##*/}.bin"
# Pick the strategy
while true; do
read -r -p "Choose a strategy: 1) auto, (2) fast, (3) hi_res, (4) ocr_only (b) back, (q) quit: " -n 1 strategy_option
echo
if [[ $strategy_option == "b" ]]; then
return
elif [[ $strategy_option == "q" ]]; then
exit 0
fi
case $strategy_option in
"1")
strategy="auto"
break
;;
"2")
strategy="fast"
break
;;
"3")
strategy="hi_res"
break
;;
"4")
strategy="ocr_only"
break
;;
*)
echo "Invalid strategy option. Please try again."
;;
esac
done
echo "Running time profile..."
python3 -m cProfile -s cumulative -o "$PROFILE_RESULTS_DIR/${test_file##*/}.prof" -m "$MODULE_PATH.run_partition" "$test_file" "$strategy"
echo "Running memory profile..."
python3 -m memray run -o "$PROFILE_RESULTS_DIR/${test_file##*/}.bin" -m "$MODULE_PATH.run_partition" "$test_file" "$strategy"
echo "Running py-spy for detailed run time profiling (this can take some time)..."
py-spy record --subprocesses -i -o "$PROFILE_RESULTS_DIR/${test_file##*/}.speedscope" --format speedscope -- python3 -m "$MODULE_PATH.run_partition" "$test_file" "$strategy"
echo "Profiling completed."
echo "Viewing results for $test_file"
echo "The py-spy produced speedscope profile can be viewed on https://www.speedscope.app or locally by installing via 'npm install -g speedscope'"
result_file=$PROFILE_RESULTS_DIR/$(basename "$test_file")
view_profile "${result_file}.bin" # Go directly to view mode
done
}
while true; do
if [[ -n "$1" ]]; then
mode="$1"
fi
if [[ -z $result_file ]]; then
read -r -p "Choose mode: (1) run, (2) view, (q) quit: " -n 1 mode
echo
fi
if [[ $mode == "1" ]]; then
run_profile
elif [[ $mode == "2" ]]; then
unset result_file # Unset the result_file variable before entering the "View" mode
view_profile
elif [[ $mode == "q" ]]; then
exit 0
else
echo "Invalid mode. Please choose 'view', 'run', or 'quit'."
fi
done
@@ -0,0 +1,266 @@
"""Quick local benchmark for PDF partition cold/warm timing.
Examples:
uv run --active --frozen --no-sync scripts/performance/quick_partition_bench.py \
--pdf example-docs/pdf/DA-1p.pdf --strategy fast --repeats 4 --warmups 1 --mode both
uv run --active --frozen --no-sync scripts/performance/quick_partition_bench.py \
--pdf example-docs/pdf/DA-1p.pdf --pdf example-docs/pdf/chevron-page.pdf \
--strategy hi_res --repeats 3 --warmups 1 --mode both
"""
import argparse
import io
import json
import statistics
import subprocess
import sys
import time
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
def _partition_once(pdf: str, strategy: str) -> dict[str, object]:
sink_out = io.StringIO()
sink_err = io.StringIO()
start = time.perf_counter()
try:
from unstructured.partition.auto import partition
with redirect_stdout(sink_out), redirect_stderr(sink_err):
elements = partition(filename=pdf, strategy=strategy)
return {
"ok": True,
"elapsed_s": time.perf_counter() - start,
"elements": len(elements),
}
except Exception as exc: # noqa: BLE001
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
def _summary(values: list[float]) -> dict[str, float]:
return {
"mean_s": statistics.mean(values),
"median_s": statistics.median(values),
"min_s": min(values),
"max_s": max(values),
"stdev_s": statistics.stdev(values) if len(values) > 1 else 0.0,
}
def _run_cold(pdf: str, strategy: str, repeats: int) -> tuple[list[float], int, list[str]]:
times: list[float] = []
elements = -1
errors: list[str] = []
for _ in range(repeats):
proc = subprocess.run(
[
sys.executable,
__file__,
"--_child",
"--pdf",
pdf,
"--strategy",
strategy,
],
capture_output=True,
text=True,
check=False,
)
lines = [line.strip() for line in (proc.stdout or "").splitlines() if line.strip()]
json_line = next((line for line in reversed(lines) if line.startswith("{")), "")
if not json_line:
stderr_tail = (proc.stderr or "").strip().splitlines()
detail = stderr_tail[-1] if stderr_tail else "no json output"
errors.append(f"child failed rc={proc.returncode} ({detail})")
continue
row = json.loads(json_line)
if bool(row.get("ok")):
times.append(float(row["elapsed_s"]))
elements = int(row["elements"])
else:
errors.append(str(row.get("error", "unknown error")))
return times, elements, errors
def _run_warm(
pdf: str,
strategy: str,
repeats: int,
warmups: int,
) -> tuple[list[float], int, list[str]]:
errors: list[str] = []
for _ in range(warmups):
row = _partition_once(pdf=pdf, strategy=strategy)
if not bool(row.get("ok")):
errors.append(str(row.get("error", "unknown error")))
return [], -1, errors
times: list[float] = []
elements = -1
for _ in range(repeats):
row = _partition_once(pdf=pdf, strategy=strategy)
if bool(row.get("ok")):
times.append(float(row["elapsed_s"]))
elements = int(row["elements"])
else:
errors.append(str(row.get("error", "unknown error")))
return times, elements, errors
def _collect_pdfs(pdf_args: list[str], pdf_dir_args: list[str]) -> list[str]:
paths = [str(Path(p)) for p in pdf_args]
for pdf_dir in pdf_dir_args:
root = Path(pdf_dir)
if not root.is_dir():
raise FileNotFoundError(f"pdf-dir does not exist: {root}")
paths.extend(str(p) for p in sorted(root.rglob("*.pdf")))
if not paths:
raise ValueError("Provide at least one --pdf or --pdf-dir")
deduped = [Path(p) for p in dict.fromkeys(paths)]
missing = [str(p) for p in deduped if not p.exists()]
if missing:
raise FileNotFoundError(f"Missing files: {', '.join(missing)}")
return [str(p) for p in deduped]
def _print_mode(label: str, values: list[float], elements: int, errors: list[str]) -> None:
if not values:
first = errors[0] if errors else "unknown error"
print(f" {label} FAILED ({first})", flush=True)
return
s = _summary(values)
print(
f" {label} mean={s['mean_s']:.4f}s median={s['median_s']:.4f}s "
f"min={s['min_s']:.4f}s max={s['max_s']:.4f}s n={len(values)} elements={elements}",
flush=True,
)
if errors:
print(f" {label} partial_failures={len(errors)} first_error={errors[0]}", flush=True)
def main() -> None:
parser = argparse.ArgumentParser(description="Quick local PDF partition benchmark")
parser.add_argument("--pdf", action="append", default=[], help="PDF path (repeatable)")
parser.add_argument("--pdf-dir", action="append", default=[], help="Directory of PDFs")
parser.add_argument("--strategy", default="fast", choices=["fast", "hi_res", "auto"])
parser.add_argument("--repeats", type=int, default=3)
parser.add_argument("--warmups", type=int, default=1)
parser.add_argument("--mode", default="both", choices=["cold", "warm", "both"])
parser.add_argument("--json-out", default="", help="Optional JSON output path")
parser.add_argument("--_child", action="store_true", help=argparse.SUPPRESS)
args = parser.parse_args()
if args._child:
print(json.dumps(_partition_once(args.pdf[0], args.strategy)), flush=True)
return
pdfs = _collect_pdfs(args.pdf, args.pdf_dir)
print(
f"strategy={args.strategy} mode={args.mode} repeats={args.repeats} "
f"warmups={args.warmups} pdf_count={len(pdfs)}",
flush=True,
)
by_mode_times: dict[str, list[float]] = {"cold": [], "warm": []}
by_mode_file_means: dict[str, list[float]] = {"cold": [], "warm": []}
by_mode_failed_files: dict[str, int] = {"cold": 0, "warm": 0}
results: list[dict[str, object]] = []
for pdf in pdfs:
row: dict[str, object] = {"pdf": pdf}
print(f"FILE {pdf}", flush=True)
if args.mode in ("cold", "both"):
times, elements, errors = _run_cold(pdf, args.strategy, args.repeats)
_print_mode("cold", times, elements, errors)
if times:
by_mode_times["cold"].extend(times)
by_mode_file_means["cold"].append(statistics.mean(times))
row["cold"] = {"ok": True, "times_s": times, "elements": elements, "errors": errors}
else:
by_mode_failed_files["cold"] += 1
row["cold"] = {"ok": False, "errors": errors}
if args.mode in ("warm", "both"):
times, elements, errors = _run_warm(pdf, args.strategy, args.repeats, args.warmups)
_print_mode("warm", times, elements, errors)
if times:
by_mode_times["warm"].extend(times)
by_mode_file_means["warm"].append(statistics.mean(times))
row["warm"] = {
"ok": True,
"times_s": times,
"elements": elements,
"errors": errors,
"warmups": args.warmups,
}
else:
by_mode_failed_files["warm"] += 1
row["warm"] = {"ok": False, "errors": errors, "warmups": args.warmups}
results.append(row)
aggregate: dict[str, object] = {}
print("AGGREGATE", flush=True)
for mode in ("cold", "warm"):
times = by_mode_times[mode]
if not times:
continue
s = _summary(times)
file_mean = statistics.mean(by_mode_file_means[mode])
succeeded = len(by_mode_file_means[mode])
failed = by_mode_failed_files[mode]
aggregate[mode] = {
"summary": s,
"file_mean_s": file_mean,
"samples": len(times),
"succeeded_files": succeeded,
"failed_files": failed,
}
print(
f" {mode} succeeded_files={succeeded} failed_files={failed} "
f"file_mean={file_mean:.4f}s mean={s['mean_s']:.4f}s median={s['median_s']:.4f}s "
f"min={s['min_s']:.4f}s max={s['max_s']:.4f}s "
f"stdev={s['stdev_s']:.4f}s samples={len(times)}",
flush=True,
)
if args.json_out:
out = Path(args.json_out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(
json.dumps(
{
"strategy": args.strategy,
"mode": args.mode,
"repeats": args.repeats,
"warmups": args.warmups,
"pdf_count": len(pdfs),
"per_file": results,
"aggregate": aggregate,
},
indent=2,
)
)
print(f"json_out={out}", flush=True)
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
flameprof>=0.4
memray>=1.7.0
snakeviz>=2.2.0
py-spy>=0.3.14
+19
View File
@@ -0,0 +1,19 @@
import os
import sys
from unstructured.partition.auto import partition
if __name__ == "__main__":
if len(sys.argv) < 3:
print(
"Please provide the path to the file as the first argument and the strategy as the "
"second argument.",
)
sys.exit(1)
file_path = sys.argv[1]
strategy = sys.argv[2]
model_name = sys.argv[3] if len(sys.argv) > 3 else os.environ.get("PARTITION_MODEL_NAME")
result = partition(file_path, strategy=strategy, model_name=model_name)
# access element in the return value to make sure we got something back, otherwise error
result[1]
+38
View File
@@ -0,0 +1,38 @@
import os
import sys
import time
from unstructured.partition.auto import partition
def warm_up_process(filename):
warmup_dir = os.path.join(os.path.dirname(__file__), "warmup-docs")
warmup_file = os.path.join(warmup_dir, f"warmup{os.path.splitext(filename)[1]}")
if os.path.exists(warmup_file):
partition(warmup_file, strategy="fast")
else:
partition(filename, strategy="fast")
def measure_execution_time(filename, iterations, strategy):
total_time = 0.0
for _ in range(iterations):
start_time = time.time()
partition(filename, strategy=strategy)
end_time = time.time()
execution_time = end_time - start_time
total_time += execution_time
average_time = total_time / iterations
print("Average time:", average_time)
if __name__ == "__main__":
filename = sys.argv[1]
iterations = int(sys.argv[2])
strategy = sys.argv[3]
warm_up_process(filename)
measure_execution_time(filename, iterations, strategy)
+1
View File
@@ -0,0 +1 @@
../../../example-docs/handbook-1p.docx
+1
View File
@@ -0,0 +1 @@
../../../example-docs/example-10k-1p.html
+1
View File
@@ -0,0 +1 @@
../../../example-docs/layout-parser-paper-fast.jpg
+1
View File
@@ -0,0 +1 @@
../../../example-docs/layout-parser-paper-fast.pdf
+1
View File
@@ -0,0 +1 @@
../../../example-docs/science-exploration-1p.pptx
+1
View File
@@ -0,0 +1 @@
../../../example-docs/book-war-and-peace-1p.txt