PyNvVideoCodec 2.0.4__cp314-cp314-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyNvVideoCodec/PyNvVideoCodec_121.cp314-win_amd64.pyd +0 -0
- PyNvVideoCodec/PyNvVideoCodec_130.cp314-win_amd64.pyd +0 -0
- PyNvVideoCodec/VersionCheck.cp314-win_amd64.pyd +0 -0
- PyNvVideoCodec/__init__.py +246 -0
- PyNvVideoCodec/avcodec-62.dll +0 -0
- PyNvVideoCodec/avdevice-62.dll +0 -0
- PyNvVideoCodec/avfilter-11.dll +0 -0
- PyNvVideoCodec/avformat-62.dll +0 -0
- PyNvVideoCodec/avutil-60.dll +0 -0
- PyNvVideoCodec/decoders/SimpleDecoder.py +230 -0
- PyNvVideoCodec/decoders/ThreadedDecoder.py +129 -0
- PyNvVideoCodec/decoders/__init__.py +22 -0
- PyNvVideoCodec/swresample-6.dll +0 -0
- PyNvVideoCodec/swscale-9.dll +0 -0
- PyNvVideoCodec/transcoder/Transcoder.py +55 -0
- PyNvVideoCodec/transcoder/__init__.py +22 -0
- benchmarks/cached_decoder_benchmark.py +813 -0
- benchmarks/frame_sampling_benchmark.py +765 -0
- benchmarks/requirements.txt +47 -0
- benchmarks/segmented_transcode_benchmark.py +1546 -0
- pynvvideocodec-2.0.4.data/data/external/ffmpeg/readme.txt +15 -0
- pynvvideocodec-2.0.4.data/data/external/ffmpeg/src/ffmpeg-8.1.1.tar.xz +0 -0
- pynvvideocodec-2.0.4.dist-info/METADATA +84 -0
- pynvvideocodec-2.0.4.dist-info/RECORD +65 -0
- pynvvideocodec-2.0.4.dist-info/WHEEL +5 -0
- pynvvideocodec-2.0.4.dist-info/licenses/LICENSE.txt +22 -0
- pynvvideocodec-2.0.4.dist-info/licenses/NOTICES.txt +167 -0
- pynvvideocodec-2.0.4.dist-info/top_level.txt +3 -0
- samples/advanced/decode.py +173 -0
- samples/advanced/decode_from_memory_buffer.py +204 -0
- samples/advanced/decode_perf.py +299 -0
- samples/advanced/decode_reconfigure.py +184 -0
- samples/advanced/decode_sei_msg.py +193 -0
- samples/advanced/decode_with_cuda_control.py +214 -0
- samples/advanced/decode_with_low_latency.py +183 -0
- samples/advanced/encode_config.json +9 -0
- samples/advanced/encode_config_lowlatency.json +11 -0
- samples/advanced/encode_perf.py +338 -0
- samples/advanced/encode_reconfigure.py +211 -0
- samples/advanced/encode_sei_msg.py +188 -0
- samples/advanced/simple_decode_stats.py +179 -0
- samples/advanced/transcode.py +288 -0
- samples/basic/create_video_segments.py +187 -0
- samples/basic/encode.py +347 -0
- samples/basic/encode_config.json +9 -0
- samples/basic/segments.txt +3 -0
- samples/basic/simple_decode_sampling.py +182 -0
- samples/basic/transcode_config.json +6 -0
- samples/jupyter/ReconfigureDecoder.png +0 -0
- samples/jupyter/SimpleDecoderAPIs.png +0 -0
- samples/jupyter/ThreadedDecoder.png +0 -0
- samples/jupyter/coco_classes.pickle +0 -0
- samples/jupyter/object_detection_tutorial.ipynb +762 -0
- samples/jupyter/simple_decode_tutorial.ipynb +651 -0
- samples/requirements.txt +78 -0
- samples/utils/Utils.py +405 -0
- samples/utils/__init__.py +36 -0
- samples/utils/decode_parser.py +642 -0
- samples/utils/decode_stats_writer.py +177 -0
- samples/utils/encode_parallel_utils.py +174 -0
- samples/utils/encode_parser.py +357 -0
- samples/utils/frame_utils.py +89 -0
- samples/utils/sei_message_parser.py +223 -0
- samples/utils/sei_parser.py +223 -0
- samples/utils/transcode_parser.py +218 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
# This copyright notice applies to this file only
|
|
2
|
+
#
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
#
|
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
7
|
+
# copy of this software and associated documentation files (the "Software"),
|
|
8
|
+
# to deal in the Software without restriction, including without limitation
|
|
9
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
10
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
|
11
|
+
# Software is furnished to do so, subject to the following conditions:
|
|
12
|
+
#
|
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
|
14
|
+
# all copies or substantial portions of the Software.
|
|
15
|
+
#
|
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
19
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
22
|
+
# DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import platform
|
|
26
|
+
import json
|
|
27
|
+
import time
|
|
28
|
+
import torch
|
|
29
|
+
import numpy as np
|
|
30
|
+
from concurrent.futures import ThreadPoolExecutor, wait
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
import PyNvVideoCodec as nvc
|
|
33
|
+
import pycuda.driver as cuda
|
|
34
|
+
import pycuda.autoinit
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
37
|
+
from abc import ABC, abstractmethod
|
|
38
|
+
from tabulate import tabulate
|
|
39
|
+
import threading
|
|
40
|
+
import subprocess
|
|
41
|
+
import argparse
|
|
42
|
+
import re
|
|
43
|
+
from tqdm import tqdm
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
Objective of this benchmark is to measure sampling performance of PyNvVideoCodec in various sampling scenarios:
|
|
47
|
+
- Sequential decoding of first N frames.
|
|
48
|
+
- Uniform sampling: Sample frames at regular intervals.
|
|
49
|
+
- Random sampling: Sample frames at random intervals.
|
|
50
|
+
|
|
51
|
+
This script performs benchmarking tests on the PyNvVideoCodec decoder frame sampling to evaluate its performance
|
|
52
|
+
under different conditions. It measures:
|
|
53
|
+
1. Decoding performance with different thread counts (1, n where n is the number of NVDecs)
|
|
54
|
+
2. Efficiency of direct frame sampling vs sequential decoding
|
|
55
|
+
3. Performance across different video configurations (resolution, GOP size)
|
|
56
|
+
|
|
57
|
+
The script creates test videos with various configurations and then runs benchmarks with:
|
|
58
|
+
- Different thread counts (1, n where n is the number of NVDecs)
|
|
59
|
+
- Different decoding patterns:
|
|
60
|
+
* Sequential decoding of first N frames
|
|
61
|
+
* Uniform sampling of frames
|
|
62
|
+
* Random sampling of frames
|
|
63
|
+
|
|
64
|
+
NOTE 1: The video clips are generated using FFmpeg only for the first run. Subsequent runs will use the generated video clips.
|
|
65
|
+
The clips are stored in the benchmark_videos directory.
|
|
66
|
+
|
|
67
|
+
NOTE 2: To saturate all the NVDECs and get maximum benefit, run the script with --nvdecs <number_of_nvdecs> as a multiple of the number of NVDecs available on the system.
|
|
68
|
+
Use this link to check the number of NVDecs available on the system: https://developer.nvidia.com/video-encode-decode-support-matrix
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
python frame_sampling_benchmark.py --nvdecs <number_of_nvdecs> --ffmpeg-dir <path> [options]
|
|
72
|
+
|
|
73
|
+
Required Arguments:
|
|
74
|
+
--nvdecs <number> Number of NVDecs available on the system
|
|
75
|
+
--ffmpeg-dir Path to the FFmpeg installation directory (bin/ffmpeg on Unix, bin/ffmpeg.exe on Windows).
|
|
76
|
+
|
|
77
|
+
Optional Arguments:
|
|
78
|
+
--verbose, -v Show detailed thread-level performance information (default: False)
|
|
79
|
+
--resolution, -res Video resolution (default: 1920x1080)
|
|
80
|
+
--gop, -g GOP sizes to test, space-separated list (default: 30 250)
|
|
81
|
+
--duration, -d Video duration in seconds (default: 30)
|
|
82
|
+
--fps, -f Video frames per second (default: 30)
|
|
83
|
+
--num-seq-frames, -seq Number of frames to decode sequentially (default: 100)
|
|
84
|
+
--num-samp-frames, -samp Number of frames to sample for uniform/random patterns (default: 30)
|
|
85
|
+
|
|
86
|
+
Please note that the following arguments are required for video generation:
|
|
87
|
+
--resolution, -res Video resolution (default: 1920x1080)
|
|
88
|
+
--gop, -g GOP sizes to test, space-separated list (default: 30 250)
|
|
89
|
+
--duration, -d Video duration in seconds (default: 30)
|
|
90
|
+
--fps, -f Video frames per second (default: 30)
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
python frame_sampling_benchmark.py --nvdecs 3 --resolution 1280x720 --gop 30 60 --verbose
|
|
94
|
+
This will run benchmarks with thread counts [1, 3], 720p resolution, GOP sizes 30 and 60,
|
|
95
|
+
and show detailed thread information.
|
|
96
|
+
|
|
97
|
+
python frame_sampling_benchmark.py --nvdecs 2 --ffmpeg-dir /path/to/ffmpeg-n7.1-linux64-lgpl-shared
|
|
98
|
+
Use FFmpeg from the given installation directory for generating benchmark videos.
|
|
99
|
+
|
|
100
|
+
Output:
|
|
101
|
+
The script generates two types of tables:
|
|
102
|
+
1. Main Results Table: Shows FPS and efficiency for each configuration
|
|
103
|
+
2. Thread Details Table (with --verbose): Shows per-thread FPS performance
|
|
104
|
+
|
|
105
|
+
Results are also saved to a JSON file named benchmark_results.json for further analysis.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class DecoderPerformance:
|
|
111
|
+
decoder_name: str
|
|
112
|
+
video_file: str
|
|
113
|
+
frames_decoded: int
|
|
114
|
+
thread_execution_times: List[float]
|
|
115
|
+
overall_fps: float
|
|
116
|
+
per_thread_fps: List[float]
|
|
117
|
+
thread_count: int
|
|
118
|
+
test_description: str
|
|
119
|
+
sampling_method: str # "sequential", "uniform", or "random"
|
|
120
|
+
sequential_decode_time: Optional[float] = None
|
|
121
|
+
sampling_efficiency: Optional[float] = None # sequential_time / actual_time
|
|
122
|
+
|
|
123
|
+
class VideoFrameDecoder(ABC):
|
|
124
|
+
def __init__(self, worker_count: int = 4):
|
|
125
|
+
self.worker_count = worker_count
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def get_frames_at_timestamps(self, video_path: str, timestamps: List[float]) -> Tuple[List[np.ndarray], List[float]]:
|
|
129
|
+
"""Retrieve video frames at specified timestamps"""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def get_sequential_frames(self, video_path: str, frame_count: int, target_timestamp: float = None) -> Tuple[List[np.ndarray], List[float]]:
|
|
134
|
+
"""Retrieve frames sequentially from the start of the video"""
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
def get_benchmark_description(self, frame_count: int, pattern_type: str) -> str:
|
|
138
|
+
"""Generate description for the benchmark test case"""
|
|
139
|
+
if pattern_type == "sequential":
|
|
140
|
+
return f"Sequential retrieval of first {frame_count} frames"
|
|
141
|
+
return f"Sampling {frame_count} frames with {pattern_type} pattern"
|
|
142
|
+
|
|
143
|
+
class PyNvVideoCodecDecoder(VideoFrameDecoder):
|
|
144
|
+
def __init__(self, worker_count: int = 4):
|
|
145
|
+
super().__init__(worker_count)
|
|
146
|
+
self.decoders = []
|
|
147
|
+
self.video_file = None
|
|
148
|
+
self.lock = threading.Lock() # For thread-safe decoder initialization
|
|
149
|
+
|
|
150
|
+
def get_frames_at_timestamps(self, video_path: str, timestamps: List[float]) -> Tuple[List[np.ndarray], List[float]]:
|
|
151
|
+
"""Retrieve video frames at specified timestamps"""
|
|
152
|
+
with self.lock:
|
|
153
|
+
if not self.decoders:
|
|
154
|
+
self.decoders = [nvc.SimpleDecoder(video_path, bWaitForSessionWarmUp=True) for _ in range(self.worker_count)]
|
|
155
|
+
else:
|
|
156
|
+
for decoder in self.decoders:
|
|
157
|
+
decoder.reconfigure_decoder(video_path)
|
|
158
|
+
self.video_file = video_path
|
|
159
|
+
|
|
160
|
+
nvc.PyNvDecoder.SetSessionCount(self.worker_count)
|
|
161
|
+
|
|
162
|
+
frames = []
|
|
163
|
+
thread_times = []
|
|
164
|
+
|
|
165
|
+
def decode_worker(decoder_idx: int, timestamps: List[float]) -> Tuple[List[np.ndarray], float]:
|
|
166
|
+
start_time = time.time()
|
|
167
|
+
batch_frames = []
|
|
168
|
+
decoder = self.decoders[decoder_idx]
|
|
169
|
+
target_indices = []
|
|
170
|
+
|
|
171
|
+
for timestamp in timestamps:
|
|
172
|
+
index = decoder.get_index_from_time_in_seconds(timestamp)
|
|
173
|
+
target_indices.append(index)
|
|
174
|
+
|
|
175
|
+
batch_frames = decoder.get_batch_frames_by_index(target_indices)
|
|
176
|
+
|
|
177
|
+
end_time = time.time()
|
|
178
|
+
elapsed_time = end_time - start_time
|
|
179
|
+
session_init_time = decoder.get_session_init_time()/1000
|
|
180
|
+
elapsed_time -= session_init_time
|
|
181
|
+
return batch_frames, elapsed_time
|
|
182
|
+
|
|
183
|
+
with ThreadPoolExecutor(max_workers=self.worker_count) as executor:
|
|
184
|
+
futures = []
|
|
185
|
+
for i in range(self.worker_count):
|
|
186
|
+
futures.append(executor.submit(decode_worker, i, timestamps))
|
|
187
|
+
|
|
188
|
+
for future in futures:
|
|
189
|
+
batch_frames, thread_time = future.result()
|
|
190
|
+
frames.extend(batch_frames)
|
|
191
|
+
thread_times.append(thread_time)
|
|
192
|
+
|
|
193
|
+
return frames, thread_times
|
|
194
|
+
|
|
195
|
+
def get_sequential_frames(self, video_path: str, frame_count: int = None, target_timestamp: float = None) -> Tuple[List[np.ndarray], List[float]]:
|
|
196
|
+
"""Retrieve frames sequentially from the start of the video"""
|
|
197
|
+
with self.lock:
|
|
198
|
+
if not self.decoders:
|
|
199
|
+
self.decoders = [nvc.SimpleDecoder(video_path, bWaitForSessionWarmUp=True) for _ in range(self.worker_count)]
|
|
200
|
+
else:
|
|
201
|
+
for decoder in self.decoders:
|
|
202
|
+
decoder.reconfigure_decoder(video_path)
|
|
203
|
+
self.video_file = video_path
|
|
204
|
+
|
|
205
|
+
nvc.PyNvDecoder.SetSessionCount(self.worker_count)
|
|
206
|
+
|
|
207
|
+
frames = []
|
|
208
|
+
thread_times = []
|
|
209
|
+
|
|
210
|
+
def decode_worker(decoder_idx: int, frame_count: int, target_timestamp: float = None) -> Tuple[List[np.ndarray], float]:
|
|
211
|
+
batch_frames = []
|
|
212
|
+
decoder = self.decoders[decoder_idx]
|
|
213
|
+
if target_timestamp:
|
|
214
|
+
frame_count = decoder.get_index_from_time_in_seconds(target_timestamp)
|
|
215
|
+
|
|
216
|
+
start_time = time.time()
|
|
217
|
+
for i in range(0, frame_count, 5):
|
|
218
|
+
batch = decoder.get_batch_frames(5)
|
|
219
|
+
batch_frames.extend(batch)
|
|
220
|
+
if len(batch_frames) >= frame_count:
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
end_time = time.time()
|
|
224
|
+
elapsed_time = end_time - start_time
|
|
225
|
+
session_init_time = decoder.get_session_init_time()/1000
|
|
226
|
+
elapsed_time -= session_init_time
|
|
227
|
+
return batch_frames[:frame_count], elapsed_time
|
|
228
|
+
|
|
229
|
+
with ThreadPoolExecutor(max_workers=self.worker_count) as executor:
|
|
230
|
+
futures = []
|
|
231
|
+
for i in range(self.worker_count):
|
|
232
|
+
futures.append(executor.submit(decode_worker, i, frame_count, target_timestamp))
|
|
233
|
+
|
|
234
|
+
for future in futures:
|
|
235
|
+
batch_frames, thread_time = future.result()
|
|
236
|
+
frames.extend(batch_frames)
|
|
237
|
+
thread_times.append(thread_time)
|
|
238
|
+
|
|
239
|
+
return frames, thread_times
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def get_system_metadata() -> Dict[str, Any]:
|
|
243
|
+
"""Get system metadata for benchmarking results"""
|
|
244
|
+
return {
|
|
245
|
+
"cpu_count": os.cpu_count(),
|
|
246
|
+
"system": platform.system(),
|
|
247
|
+
"machine": platform.machine(),
|
|
248
|
+
"python_version": str(platform.python_version()),
|
|
249
|
+
"cuda": torch.cuda.get_device_properties(0).name if torch.cuda.is_available() else "not available",
|
|
250
|
+
"cuda_available": torch.cuda.is_available()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
def parse_ffmpeg_progress(line: str) -> Optional[Dict[str, str]]:
|
|
254
|
+
"""Parse FFmpeg progress line and extract key information"""
|
|
255
|
+
if not line.strip():
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
# FFmpeg progress format: key=value key=value ...
|
|
259
|
+
# We're interested in lines that contain progress data
|
|
260
|
+
progress_data = {}
|
|
261
|
+
|
|
262
|
+
# Handle both single line progress (frame=123) and multi-key progress lines
|
|
263
|
+
if '=' in line:
|
|
264
|
+
for item in line.split():
|
|
265
|
+
if '=' in item:
|
|
266
|
+
key, value = item.split('=', 1)
|
|
267
|
+
progress_data[key] = value
|
|
268
|
+
|
|
269
|
+
return progress_data if progress_data else None
|
|
270
|
+
|
|
271
|
+
def generate_mandelbrot_video(
|
|
272
|
+
width: int, height: int, duration: int, fps: int, gop: int, output_path: str,
|
|
273
|
+
ffmpeg_path: str = "ffmpeg"
|
|
274
|
+
) -> str:
|
|
275
|
+
"""
|
|
276
|
+
Generate a Mandelbrot set video using FFmpeg with progress tracking.
|
|
277
|
+
|
|
278
|
+
Parameters:
|
|
279
|
+
width (int): Video width
|
|
280
|
+
height (int): Video height
|
|
281
|
+
duration (int): Video duration in seconds
|
|
282
|
+
fps (int): Frames per second
|
|
283
|
+
gop (int): GOP size
|
|
284
|
+
output_path (str): Path to save the generated video
|
|
285
|
+
ffmpeg_path (str): Path to the ffmpeg executable (resolved from the required
|
|
286
|
+
--ffmpeg-dir as <dir>/bin/ffmpeg on Unix, <dir>/bin/ffmpeg.exe on Windows).
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
str: Path to the generated video
|
|
290
|
+
"""
|
|
291
|
+
print(f"Generating Mandelbrot set video at {width}x{height}, {duration}s duration, {fps} fps, {gop} GOP size")
|
|
292
|
+
|
|
293
|
+
# Create the directory if it doesn't exist
|
|
294
|
+
os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
|
|
295
|
+
|
|
296
|
+
# FFmpeg command: ffmpeg_path is the executable (from --ffmpeg-dir: bin/ffmpeg or bin/ffmpeg.exe)
|
|
297
|
+
cmd = [
|
|
298
|
+
ffmpeg_path, "-y",
|
|
299
|
+
"-progress", "pipe:2", # Output progress to stderr
|
|
300
|
+
"-v", "warning",
|
|
301
|
+
# Video input from mandelbrot filter
|
|
302
|
+
"-f", "lavfi",
|
|
303
|
+
"-i", f"mandelbrot=s={width}x{height}",
|
|
304
|
+
# Set duration
|
|
305
|
+
"-t", str(duration),
|
|
306
|
+
# Video encoding with h264 and specific parameters
|
|
307
|
+
"-c:v", "h264_nvenc",
|
|
308
|
+
"-preset", "medium",
|
|
309
|
+
"-b:v", "8M",
|
|
310
|
+
"-bf", "3", # 3 B-frames
|
|
311
|
+
"-g", str(gop), # GOP size
|
|
312
|
+
"-rc", "vbr", # Variable bitrate
|
|
313
|
+
"-profile:v", "high", # High profile for better quality
|
|
314
|
+
output_path
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
# For custom FFmpeg builds, ensure the subprocess finds shared libs:
|
|
318
|
+
# - Unix: add <install_dir>/lib to LD_LIBRARY_PATH.
|
|
319
|
+
# - Windows: add <install_dir>/bin to PATH.
|
|
320
|
+
env = os.environ.copy()
|
|
321
|
+
if os.path.isfile(ffmpeg_path):
|
|
322
|
+
ffmpeg_dir = os.path.dirname(ffmpeg_path)
|
|
323
|
+
if platform.system() == "Windows":
|
|
324
|
+
# DLLs are typically in the same bin/ folder as ffmpeg.exe
|
|
325
|
+
env["PATH"] = ffmpeg_dir + os.pathsep + env.get("PATH", "")
|
|
326
|
+
else:
|
|
327
|
+
# Check for .../bin/ffmpeg layout and add sibling lib/ to LD_LIBRARY_PATH
|
|
328
|
+
if os.path.sep + "bin" + os.path.sep in ffmpeg_path:
|
|
329
|
+
install_dir = os.path.dirname(ffmpeg_dir)
|
|
330
|
+
lib_dir = os.path.join(install_dir, "lib")
|
|
331
|
+
if os.path.isdir(lib_dir):
|
|
332
|
+
env["LD_LIBRARY_PATH"] = lib_dir + (os.pathsep + env.get("LD_LIBRARY_PATH", ""))
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
# Calculate estimated total frames for progress tracking
|
|
336
|
+
estimated_frames = duration * fps
|
|
337
|
+
|
|
338
|
+
print("Starting FFmpeg process to generate Mandelbrot video...")
|
|
339
|
+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
340
|
+
universal_newlines=True, bufsize=1, env=env)
|
|
341
|
+
|
|
342
|
+
# Progress bar setup - single line that updates in place
|
|
343
|
+
pbar = tqdm(total=estimated_frames, unit='frames',
|
|
344
|
+
desc=f"Generating {os.path.basename(output_path)}",
|
|
345
|
+
ncols=80, leave=True, position=0)
|
|
346
|
+
|
|
347
|
+
# Read stderr line by line to get progress
|
|
348
|
+
current_frame = 0
|
|
349
|
+
max_frame_seen = 0
|
|
350
|
+
last_update_time = time.time()
|
|
351
|
+
update_interval = 0.5 # Update progress bar every 0.5 seconds maximum
|
|
352
|
+
|
|
353
|
+
while True:
|
|
354
|
+
line = process.stderr.readline()
|
|
355
|
+
if not line:
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
# Parse progress information
|
|
359
|
+
progress_data = parse_ffmpeg_progress(line.strip())
|
|
360
|
+
if progress_data and 'frame' in progress_data:
|
|
361
|
+
try:
|
|
362
|
+
frame_num = int(progress_data['frame'])
|
|
363
|
+
max_frame_seen = max(max_frame_seen, frame_num)
|
|
364
|
+
|
|
365
|
+
# Only update progress bar if enough time has passed or significant progress made
|
|
366
|
+
current_time = time.time()
|
|
367
|
+
if (current_time - last_update_time >= update_interval or
|
|
368
|
+
frame_num - current_frame >= 10):
|
|
369
|
+
|
|
370
|
+
# If we see more frames than expected, adjust the total
|
|
371
|
+
if frame_num > estimated_frames:
|
|
372
|
+
pbar.total = frame_num + 50 # Add buffer for final frames
|
|
373
|
+
|
|
374
|
+
if frame_num > current_frame:
|
|
375
|
+
# Update progress bar
|
|
376
|
+
pbar.update(frame_num - current_frame)
|
|
377
|
+
current_frame = frame_num
|
|
378
|
+
last_update_time = current_time
|
|
379
|
+
|
|
380
|
+
except (ValueError, KeyError):
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# Wait for process to complete
|
|
384
|
+
process.wait()
|
|
385
|
+
|
|
386
|
+
# Ensure progress bar reaches 100%
|
|
387
|
+
if max_frame_seen > current_frame:
|
|
388
|
+
pbar.update(max_frame_seen - current_frame)
|
|
389
|
+
current_frame = max_frame_seen
|
|
390
|
+
|
|
391
|
+
# If we still haven't reached the total, complete it
|
|
392
|
+
if current_frame < pbar.total:
|
|
393
|
+
pbar.update(pbar.total - current_frame)
|
|
394
|
+
|
|
395
|
+
pbar.close()
|
|
396
|
+
|
|
397
|
+
if process.returncode != 0:
|
|
398
|
+
# Read any remaining stderr
|
|
399
|
+
remaining_stderr = process.stderr.read()
|
|
400
|
+
print(f"Error generating video: {remaining_stderr}")
|
|
401
|
+
raise subprocess.CalledProcessError(process.returncode, cmd, remaining_stderr)
|
|
402
|
+
|
|
403
|
+
# Use regular print since progress bar is closed
|
|
404
|
+
print(f"✓ Video generation complete: {os.path.basename(output_path)}")
|
|
405
|
+
print(f" Video codec: h264_nvenc with {gop} GOP size and 3 B-frames")
|
|
406
|
+
print(f" Frames encoded: {max_frame_seen}")
|
|
407
|
+
return output_path
|
|
408
|
+
|
|
409
|
+
except subprocess.CalledProcessError as e:
|
|
410
|
+
print(f"FFmpeg error: {e}")
|
|
411
|
+
raise
|
|
412
|
+
except Exception as e:
|
|
413
|
+
print(f"Error: {e}")
|
|
414
|
+
raise
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def generate_benchmark_videos(
|
|
418
|
+
resolution: str,
|
|
419
|
+
duration: int,
|
|
420
|
+
fps: int,
|
|
421
|
+
gop_sizes: List[int],
|
|
422
|
+
output_dir: str,
|
|
423
|
+
ffmpeg_path: str = "ffmpeg",
|
|
424
|
+
) -> List[str]:
|
|
425
|
+
"""
|
|
426
|
+
Generate benchmark videos with different GOP sizes using FFmpeg.
|
|
427
|
+
|
|
428
|
+
Parameters:
|
|
429
|
+
resolution (str): Video resolution (e.g., '1920x1080')
|
|
430
|
+
duration (int): Video duration in seconds
|
|
431
|
+
fps (int): Frames per second
|
|
432
|
+
gop_sizes (List[int]): List of GOP sizes to test
|
|
433
|
+
output_dir (str): Directory to save the generated videos
|
|
434
|
+
ffmpeg_path (str): Path to the ffmpeg executable (resolved from the required
|
|
435
|
+
--ffmpeg-dir as <dir>/bin/ffmpeg on Unix, <dir>/bin/ffmpeg.exe on Windows).
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
List[str]: List of paths to the generated videos
|
|
439
|
+
"""
|
|
440
|
+
width, height = map(int, resolution.split('x'))
|
|
441
|
+
generated_videos = []
|
|
442
|
+
|
|
443
|
+
print(f"\nGenerating {len(gop_sizes)} benchmark videos...")
|
|
444
|
+
|
|
445
|
+
# Create a clean progress bar for overall video generation
|
|
446
|
+
with tqdm(total=len(gop_sizes), desc="Overall Progress",
|
|
447
|
+
ncols=60, leave=True, position=1) as overall_pbar:
|
|
448
|
+
for gop in gop_sizes:
|
|
449
|
+
output_path = f"{output_dir}/mandelbrot_{resolution}_{duration}s_{fps}fps_{gop}gop.mp4"
|
|
450
|
+
|
|
451
|
+
# Skip if video already exists
|
|
452
|
+
if os.path.exists(output_path):
|
|
453
|
+
overall_pbar.write(f"⏭️ Skipping {os.path.basename(output_path)} - already exists")
|
|
454
|
+
generated_videos.append(output_path)
|
|
455
|
+
overall_pbar.update(1)
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
# Use the ffmpeg executable (resolved from --ffmpeg-dir or system) for video generation
|
|
460
|
+
video_path = generate_mandelbrot_video(
|
|
461
|
+
width, height, duration, fps, gop, output_path, ffmpeg_path=ffmpeg_path
|
|
462
|
+
)
|
|
463
|
+
generated_videos.append(video_path)
|
|
464
|
+
overall_pbar.update(1)
|
|
465
|
+
except Exception as e:
|
|
466
|
+
overall_pbar.write(f"❌ Failed to generate video with GOP {gop}: {e}")
|
|
467
|
+
overall_pbar.update(1)
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
print(f"✅ Generated {len(generated_videos)} videos successfully!")
|
|
471
|
+
return generated_videos
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def get_video_metadata(video_file: str) -> Dict[str, Any]:
|
|
475
|
+
"""Get metadata for a video file using PyNvVideoCodec"""
|
|
476
|
+
decoder = nvc.SimpleDecoder(video_file)
|
|
477
|
+
metadata = decoder.get_stream_metadata()
|
|
478
|
+
return {
|
|
479
|
+
"duration": metadata.duration,
|
|
480
|
+
"width": metadata.width,
|
|
481
|
+
"height": metadata.height,
|
|
482
|
+
"fps": metadata.average_fps,
|
|
483
|
+
"codec": metadata.codec_name
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def create_timestamp_list(video_file: str, num_samples: int, kind: str = "uniform") -> List[float]:
|
|
488
|
+
"""Generate list of timestamps for frame sampling"""
|
|
489
|
+
metadata = get_video_metadata(video_file)
|
|
490
|
+
duration = metadata["duration"]
|
|
491
|
+
|
|
492
|
+
if kind == "uniform":
|
|
493
|
+
return [i * duration / num_samples for i in range(num_samples)]
|
|
494
|
+
elif kind == "random":
|
|
495
|
+
# Generate unique random indices and scale to duration
|
|
496
|
+
indices = torch.randperm(int(duration * 1000))[:num_samples] # Convert to milliseconds for more precision
|
|
497
|
+
return (indices.float() / 1000).tolist() # Convert back to seconds
|
|
498
|
+
else:
|
|
499
|
+
raise ValueError(f"Unknown sampling kind: {kind}")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def run_benchmark_test(
|
|
503
|
+
decoder: VideoFrameDecoder,
|
|
504
|
+
video_file: str,
|
|
505
|
+
num_threads: int,
|
|
506
|
+
num_samples: int,
|
|
507
|
+
decode_pattern: str = "first_n_frames"
|
|
508
|
+
) -> DecoderPerformance:
|
|
509
|
+
"""Run a single benchmark test for a decoder"""
|
|
510
|
+
description = ""
|
|
511
|
+
sequential_time = None
|
|
512
|
+
efficiency = None
|
|
513
|
+
|
|
514
|
+
if decode_pattern == "first_n_frames":
|
|
515
|
+
description = decoder.get_benchmark_description(num_samples, "sequential")
|
|
516
|
+
frames, thread_times = decoder.get_sequential_frames(video_file, frame_count=num_samples)
|
|
517
|
+
pattern_str = f"first_{num_samples}_frames"
|
|
518
|
+
else: # uniform or random
|
|
519
|
+
description = decoder.get_benchmark_description(num_samples, decode_pattern)
|
|
520
|
+
timestamp_list = create_timestamp_list(video_file, num_samples, decode_pattern)
|
|
521
|
+
|
|
522
|
+
# First measure sequential decode time to the last timestamp
|
|
523
|
+
last_timestamp = max(timestamp_list)
|
|
524
|
+
_, seq_thread_times = decoder.get_sequential_frames(video_file, frame_count=None, target_timestamp=last_timestamp)
|
|
525
|
+
sequential_time = sum(seq_thread_times)
|
|
526
|
+
|
|
527
|
+
# Then measure random/uniform decode time
|
|
528
|
+
frames, thread_times = decoder.get_frames_at_timestamps(video_file, timestamp_list)
|
|
529
|
+
efficiency = sequential_time / sum(thread_times) if sum(thread_times) > 0 else 0
|
|
530
|
+
pattern_str = f"{num_samples}_{decode_pattern}_frames"
|
|
531
|
+
|
|
532
|
+
total_frames = len(frames)
|
|
533
|
+
thread_fps = [num_samples / t for t in thread_times]
|
|
534
|
+
total_fps = sum(thread_fps)
|
|
535
|
+
|
|
536
|
+
return DecoderPerformance(
|
|
537
|
+
decoder_name=decoder.__class__.__name__,
|
|
538
|
+
video_file=video_file,
|
|
539
|
+
frames_decoded=total_frames,
|
|
540
|
+
thread_execution_times=thread_times,
|
|
541
|
+
overall_fps=total_fps,
|
|
542
|
+
per_thread_fps=thread_fps,
|
|
543
|
+
thread_count=num_threads,
|
|
544
|
+
test_description=description,
|
|
545
|
+
sampling_method=pattern_str,
|
|
546
|
+
sequential_decode_time=sequential_time,
|
|
547
|
+
sampling_efficiency=efficiency
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def display_benchmark_results(results: List[DecoderPerformance], verbose: bool = False) -> None:
|
|
552
|
+
"""Display benchmark results in formatted tables"""
|
|
553
|
+
|
|
554
|
+
videos = sorted(set(r.video_file for r in results))
|
|
555
|
+
patterns = sorted(set(r.sampling_method for r in results))
|
|
556
|
+
thread_counts = sorted(set(r.thread_count for r in results))
|
|
557
|
+
|
|
558
|
+
print("\nBenchmark Results:")
|
|
559
|
+
|
|
560
|
+
table_data = []
|
|
561
|
+
headers = ["Video Config", "Pattern"]
|
|
562
|
+
|
|
563
|
+
for thread_count in thread_counts:
|
|
564
|
+
headers.extend([f"T{thread_count} FPS", f"T{thread_count} Eff"])
|
|
565
|
+
|
|
566
|
+
for video in videos:
|
|
567
|
+
# Get the filename without path and extension
|
|
568
|
+
filename = os.path.basename(video)
|
|
569
|
+
# Split by underscore and remove .mp4 from the last part
|
|
570
|
+
parts = filename.split('_')
|
|
571
|
+
resolution = parts[1]
|
|
572
|
+
duration = parts[2].replace('s', '')
|
|
573
|
+
gop = parts[4].replace('gop.mp4', '')
|
|
574
|
+
config_str = f"{resolution} {gop}gop {duration}s"
|
|
575
|
+
|
|
576
|
+
for pattern in patterns:
|
|
577
|
+
row = [config_str, pattern]
|
|
578
|
+
|
|
579
|
+
for thread_count in thread_counts:
|
|
580
|
+
result = next((r for r in results
|
|
581
|
+
if r.video_file == video
|
|
582
|
+
and r.sampling_method == pattern
|
|
583
|
+
and r.thread_count == thread_count), None)
|
|
584
|
+
if result:
|
|
585
|
+
row.append(f"{result.overall_fps:.1f}")
|
|
586
|
+
if result.sampling_efficiency is not None:
|
|
587
|
+
row.append(f"{result.sampling_efficiency:.2f}x")
|
|
588
|
+
else:
|
|
589
|
+
row.append("N/A")
|
|
590
|
+
else:
|
|
591
|
+
row.append("N/A")
|
|
592
|
+
row.append("N/A")
|
|
593
|
+
|
|
594
|
+
table_data.append(row)
|
|
595
|
+
|
|
596
|
+
print(tabulate(table_data, headers=headers, tablefmt="grid"))
|
|
597
|
+
|
|
598
|
+
# Only show thread-level details if verbose mode is enabled
|
|
599
|
+
if verbose:
|
|
600
|
+
print(f"\nThread Details:")
|
|
601
|
+
|
|
602
|
+
thread_headers = ["Config", "Threads", "FPS per Thread"]
|
|
603
|
+
thread_data = []
|
|
604
|
+
|
|
605
|
+
for video in videos:
|
|
606
|
+
# Get the filename without path and extension
|
|
607
|
+
filename = os.path.basename(video)
|
|
608
|
+
# Split by underscore and remove .mp4 from the last part
|
|
609
|
+
parts = filename.split('_')
|
|
610
|
+
resolution = parts[1]
|
|
611
|
+
duration = parts[2].replace('s', '')
|
|
612
|
+
gop = parts[4].replace('gop.mp4', '')
|
|
613
|
+
config_str = f"{resolution} {gop}gop {duration}s"
|
|
614
|
+
|
|
615
|
+
for pattern in patterns:
|
|
616
|
+
for thread_count in thread_counts:
|
|
617
|
+
result = next((r for r in results
|
|
618
|
+
if r.video_file == video
|
|
619
|
+
and r.sampling_method == pattern
|
|
620
|
+
and r.thread_count == thread_count), None)
|
|
621
|
+
|
|
622
|
+
if result:
|
|
623
|
+
thread_fps_str = ",".join([f"{fps:.1f}" for fps in result.per_thread_fps])
|
|
624
|
+
thread_data.append([
|
|
625
|
+
f"{config_str} {pattern}",
|
|
626
|
+
thread_count,
|
|
627
|
+
thread_fps_str
|
|
628
|
+
])
|
|
629
|
+
|
|
630
|
+
# Print thread-level details table
|
|
631
|
+
print(tabulate(thread_data, headers=thread_headers, tablefmt="grid"))
|
|
632
|
+
|
|
633
|
+
print("\nEfficiency Explanation:")
|
|
634
|
+
print("Efficiency (Eff) represents the performance comparison between two approaches:")
|
|
635
|
+
print("1. Direct sampling: Decoding specific frames directly using seek operations")
|
|
636
|
+
print("2. Sequential decode + sampling: Decoding all frames sequentially and then sampling the required frames")
|
|
637
|
+
print("The efficiency value shows how much faster direct sampling is compared to sequential decoding with sampling.")
|
|
638
|
+
print("Higher efficiency values indicate better performance of direct sampling approach.")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def save_benchmark_results(results: List[DecoderPerformance], output_file: str = "benchmark_results.json") -> None:
|
|
642
|
+
"""Save benchmark results to a JSON file"""
|
|
643
|
+
output_data = {
|
|
644
|
+
"system_info": get_system_metadata(),
|
|
645
|
+
"benchmark_results": [
|
|
646
|
+
{
|
|
647
|
+
"decoder": r.decoder_name,
|
|
648
|
+
"video": r.video_file,
|
|
649
|
+
"frames_decoded": r.frames_decoded,
|
|
650
|
+
"thread_execution_times": r.thread_execution_times,
|
|
651
|
+
"overall_fps": r.overall_fps,
|
|
652
|
+
"per_thread_fps": r.per_thread_fps,
|
|
653
|
+
"thread_count": r.thread_count,
|
|
654
|
+
"test_description": r.test_description,
|
|
655
|
+
"sampling_method": r.sampling_method,
|
|
656
|
+
"sequential_decode_time": r.sequential_decode_time,
|
|
657
|
+
"sampling_efficiency": r.sampling_efficiency
|
|
658
|
+
}
|
|
659
|
+
for r in results
|
|
660
|
+
]
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
with open(output_file, "w") as f:
|
|
664
|
+
json.dump(output_data, f, indent=4)
|
|
665
|
+
|
|
666
|
+
print(f"\nBenchmark results saved to {output_file}")
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def main():
|
|
670
|
+
# Parse command line arguments
|
|
671
|
+
parser = argparse.ArgumentParser(description='Frame Sampling Benchmarking Script')
|
|
672
|
+
parser.add_argument('--nvdecs', type=int, required=True,
|
|
673
|
+
help='Number of NVDecs available on the system')
|
|
674
|
+
parser.add_argument('--ffmpeg-dir', type=str, required=True,
|
|
675
|
+
help='Path to the FFmpeg installation directory (bin/ffmpeg on Unix, bin/ffmpeg.exe on Windows).')
|
|
676
|
+
parser.add_argument('--verbose', '-v', action='store_true',
|
|
677
|
+
help='Show detailed thread-level performance information (default: False)')
|
|
678
|
+
parser.add_argument('--resolution', '-res', type=str, default='1920x1080',
|
|
679
|
+
help='Video resolution (default: 1920x1080)')
|
|
680
|
+
parser.add_argument('--gop', '-g', type=int, nargs='+', default=[30, 250],
|
|
681
|
+
help='GOP sizes to test (space-separated list) (default: 30 250)')
|
|
682
|
+
parser.add_argument('--duration', '-d', type=int, default=30,
|
|
683
|
+
help='Video duration in seconds (default: 30)')
|
|
684
|
+
parser.add_argument('--fps', '-f', type=int, default=30,
|
|
685
|
+
help='Video frames per second (default: 30)')
|
|
686
|
+
parser.add_argument('--num-seq-frames', '-seq', type=int, default=100,
|
|
687
|
+
help='Number of frames to decode sequentially (default: 100)')
|
|
688
|
+
parser.add_argument('--num-samp-frames', '-samp', type=int, default=30,
|
|
689
|
+
help='Number of frames to sample for uniform/random patterns (default: 30)')
|
|
690
|
+
args = parser.parse_args()
|
|
691
|
+
|
|
692
|
+
# Resolve FFmpeg executable from the required installation directory
|
|
693
|
+
bin_dir = os.path.join(args.ffmpeg_dir, "bin")
|
|
694
|
+
# On Windows the executable is ffmpeg.exe; on Unix it is ffmpeg
|
|
695
|
+
if platform.system() == "Windows":
|
|
696
|
+
ffmpeg_exe = os.path.join(bin_dir, "ffmpeg.exe")
|
|
697
|
+
if not os.path.isfile(ffmpeg_exe):
|
|
698
|
+
ffmpeg_exe = os.path.join(bin_dir, "ffmpeg")
|
|
699
|
+
else:
|
|
700
|
+
ffmpeg_exe = os.path.join(bin_dir, "ffmpeg")
|
|
701
|
+
if not os.path.isfile(ffmpeg_exe):
|
|
702
|
+
expected = "bin/ffmpeg.exe or bin/ffmpeg" if platform.system() == "Windows" else "bin/ffmpeg"
|
|
703
|
+
raise FileNotFoundError(
|
|
704
|
+
f"FFmpeg executable not found at {ffmpeg_exe}. "
|
|
705
|
+
f"The given FFmpeg installation directory must contain {expected}."
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
# Set thread counts based on number of NVDecs
|
|
709
|
+
thread_counts = [1, args.nvdecs] if args.nvdecs > 1 else [1]
|
|
710
|
+
print(f"Using thread counts: {thread_counts} based on {args.nvdecs} NVDecs")
|
|
711
|
+
|
|
712
|
+
start_time = time.time()
|
|
713
|
+
|
|
714
|
+
# Create videos directory in the same location as the script
|
|
715
|
+
script_dir = Path(__file__).parent
|
|
716
|
+
videos_dir_path = script_dir / "benchmark_videos"
|
|
717
|
+
videos_dir_path.mkdir(parents=True, exist_ok=True)
|
|
718
|
+
print(f"Using videos directory: {videos_dir_path}")
|
|
719
|
+
|
|
720
|
+
# Generate benchmark videos using FFmpeg from the required --ffmpeg-dir
|
|
721
|
+
video_files = generate_benchmark_videos(
|
|
722
|
+
args.resolution,
|
|
723
|
+
args.duration,
|
|
724
|
+
args.fps,
|
|
725
|
+
args.gop,
|
|
726
|
+
str(videos_dir_path),
|
|
727
|
+
ffmpeg_path=ffmpeg_exe,
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Benchmark parameters
|
|
731
|
+
decode_patterns = ["first_n_frames", "uniform", "random"]
|
|
732
|
+
|
|
733
|
+
results = []
|
|
734
|
+
|
|
735
|
+
# Run benchmarks
|
|
736
|
+
for video_file in video_files:
|
|
737
|
+
if not os.path.exists(video_file):
|
|
738
|
+
print(f"Skipping {video_file} - file not found")
|
|
739
|
+
continue
|
|
740
|
+
|
|
741
|
+
for num_threads in thread_counts:
|
|
742
|
+
print(f"\nBenchmarking {video_file} with {num_threads} threads")
|
|
743
|
+
|
|
744
|
+
# PyNvVideoCodec benchmark
|
|
745
|
+
pynv_decoder = PyNvVideoCodecDecoder(num_threads)
|
|
746
|
+
for pattern in decode_patterns:
|
|
747
|
+
num_samples = args.num_seq_frames if pattern == "first_n_frames" else args.num_samp_frames
|
|
748
|
+
pynv_result = run_benchmark_test(pynv_decoder, video_file, num_threads, num_samples, pattern)
|
|
749
|
+
results.append(pynv_result)
|
|
750
|
+
|
|
751
|
+
# Display and save results
|
|
752
|
+
display_benchmark_results(results, verbose=args.verbose)
|
|
753
|
+
save_benchmark_results(results)
|
|
754
|
+
|
|
755
|
+
# Display total execution time
|
|
756
|
+
end_time = time.time()
|
|
757
|
+
total_time = end_time - start_time
|
|
758
|
+
hours = int(total_time // 3600)
|
|
759
|
+
minutes = int((total_time % 3600) // 60)
|
|
760
|
+
seconds = total_time % 60
|
|
761
|
+
print(f"\nTotal execution time: {hours:02d}:{minutes:02d}:{seconds:06.3f} ({total_time:.3f} seconds)")
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
if __name__ == "__main__":
|
|
765
|
+
main()
|