1use std::fs;
2use std::hint::black_box;
3use std::io::{Error as IoError, ErrorKind, Result as IoResult};
4use std::path::PathBuf;
5use std::time::{Duration, Instant};
6
7use webp_rust::decoder::{
8 decode_animation_webp, decode_lossless_webp_to_rgba, decode_lossy_webp_to_rgba,
9 decode_lossy_webp_to_yuv,
10};
11
12const DEFAULT_BATCHES: usize = 7;
13const DEFAULT_BATCH_MILLIS: u64 = 250;
14const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
15const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
16
17#[derive(Debug)]
18struct Options {
19 batches: usize,
20 batch_duration: Duration,
21 output: Option<PathBuf>,
22}
23
24#[derive(Debug)]
25struct Measurement {
26 case_name: &'static str,
27 format: &'static str,
28 width: usize,
29 height: usize,
30 pixels_per_decode: usize,
31 output_hash: u64,
32 median_ns: f64,
33 p95_ns: f64,
34}
35
36#[derive(Debug, Clone, Copy)]
37struct CaseMetadata {
38 case_name: &'static str,
39 format: &'static str,
40 width: usize,
41 height: usize,
42 pixels_per_decode: usize,
43 output_hash: u64,
44}
45
46fn invalid_input(message: impl Into<String>) -> IoError {
47 IoError::new(ErrorKind::InvalidInput, message.into())
48}
49
50fn parse_usize(value: Option<String>, name: &str) -> IoResult<usize> {
51 let value = value.ok_or_else(|| invalid_input(format!("missing value for {name}")))?;
52 value
53 .parse::<usize>()
54 .map_err(|_| invalid_input(format!("invalid value for {name}: {value}")))
55}
56
57fn parse_options() -> IoResult<Options> {
58 let mut options = Options {
59 batches: DEFAULT_BATCHES,
60 batch_duration: Duration::from_millis(DEFAULT_BATCH_MILLIS),
61 output: None,
62 };
63 let mut args = std::env::args().skip(1);
64 while let Some(arg) = args.next() {
65 match arg.as_str() {
66 "--batches" => {
67 options.batches = parse_usize(args.next(), "--batches")?;
68 if options.batches == 0 {
69 return Err(invalid_input("--batches must be greater than zero"));
70 }
71 }
72 "--batch-ms" => {
73 let millis = parse_usize(args.next(), "--batch-ms")?;
74 if millis == 0 {
75 return Err(invalid_input("--batch-ms must be greater than zero"));
76 }
77 options.batch_duration = Duration::from_millis(millis as u64);
78 }
79 "--output" => {
80 let path = args
81 .next()
82 .ok_or_else(|| invalid_input("missing value for --output"))?;
83 options.output = Some(PathBuf::from(path));
84 }
85 _ => return Err(invalid_input(format!("unknown argument: {arg}"))),
86 }
87 }
88 Ok(options)
89}
90
91fn hash_bytes(mut hash: u64, bytes: &[u8]) -> u64 {
92 for &byte in bytes {
93 hash ^= u64::from(byte);
94 hash = hash.wrapping_mul(FNV_PRIME);
95 }
96 hash
97}
98
99fn hash_usize(hash: u64, value: usize) -> u64 {
100 hash_bytes(hash, &value.to_le_bytes())
101}
102
103fn calibrate_iterations<F>(batch_duration: Duration, decode: &mut F) -> IoResult<usize>
104where
105 F: FnMut() -> IoResult<usize>,
106{
107 let mut iterations = 1usize;
108 loop {
109 let started = Instant::now();
110 for _ in 0..iterations {
111 black_box(decode()?);
112 }
113 let elapsed = started.elapsed();
114 if elapsed >= batch_duration {
115 return Ok(iterations);
116 }
117 let elapsed_nanos = elapsed.as_nanos().max(1);
118 let target_nanos = batch_duration.as_nanos();
119 let scale = target_nanos.div_ceil(elapsed_nanos) as usize;
120 iterations = iterations.saturating_mul(scale.clamp(2, 16));
121 }
122}
123
124fn measure<F>(options: &Options, metadata: CaseMetadata, mut decode: F) -> IoResult<Measurement>
125where
126 F: FnMut() -> IoResult<usize>,
127{
128 let iterations = calibrate_iterations(options.batch_duration, &mut decode)?;
129 let mut samples = Vec::with_capacity(options.batches);
130 for _ in 0..options.batches {
131 let started = Instant::now();
132 for _ in 0..iterations {
133 black_box(decode()?);
134 }
135 samples.push(started.elapsed().as_nanos() as f64 / iterations as f64);
136 }
137 samples.sort_by(f64::total_cmp);
138 let median_ns = samples[samples.len() / 2];
139 let p95_index = ((samples.len() - 1) * 95).div_ceil(100);
140 Ok(Measurement {
141 case_name: metadata.case_name,
142 format: metadata.format,
143 width: metadata.width,
144 height: metadata.height,
145 pixels_per_decode: metadata.pixels_per_decode,
146 output_hash: metadata.output_hash,
147 median_ns,
148 p95_ns: samples[p95_index],
149 })
150}
151
152fn main() -> IoResult<()> {
153 let options = parse_options()?;
154 let lossy = include_bytes!("../samples/sample.webp");
155 let lossless = include_bytes!("../samples/sample_lossless.webp");
156 let animation = include_bytes!("../samples/sample_animation.webp");
157
158 let lossy_rgba = decode_lossy_webp_to_rgba(lossy)
159 .map_err(|error| IoError::new(ErrorKind::InvalidData, error))?;
160 let lossy_rgba_hash = hash_bytes(FNV_OFFSET, &lossy_rgba.rgba);
161 let lossy_rgba_width = lossy_rgba.width;
162 let lossy_rgba_height = lossy_rgba.height;
163
164 let lossy_yuv = decode_lossy_webp_to_yuv(lossy)
165 .map_err(|error| IoError::new(ErrorKind::InvalidData, error))?;
166 let mut lossy_yuv_hash = hash_bytes(FNV_OFFSET, &lossy_yuv.y);
167 lossy_yuv_hash = hash_bytes(lossy_yuv_hash, &lossy_yuv.u);
168 lossy_yuv_hash = hash_bytes(lossy_yuv_hash, &lossy_yuv.v);
169 lossy_yuv_hash = hash_usize(lossy_yuv_hash, lossy_yuv.y_stride);
170 lossy_yuv_hash = hash_usize(lossy_yuv_hash, lossy_yuv.uv_stride);
171
172 let lossless_rgba = decode_lossless_webp_to_rgba(lossless)
173 .map_err(|error| IoError::new(ErrorKind::InvalidData, error))?;
174 let lossless_rgba_hash = hash_bytes(FNV_OFFSET, &lossless_rgba.rgba);
175 let lossless_width = lossless_rgba.width;
176 let lossless_height = lossless_rgba.height;
177
178 let decoded_animation = decode_animation_webp(animation)
179 .map_err(|error| IoError::new(ErrorKind::InvalidData, error))?;
180 let mut animation_hash = hash_usize(FNV_OFFSET, decoded_animation.loop_count as usize);
181 for frame in &decoded_animation.frames {
182 animation_hash = hash_usize(animation_hash, frame.duration);
183 animation_hash = hash_bytes(animation_hash, &frame.rgba);
184 }
185 let animation_width = decoded_animation.width;
186 let animation_height = decoded_animation.height;
187 let animation_pixels = animation_width * animation_height * decoded_animation.frames.len();
188
189 let measurements = [
190 measure(
191 &options,
192 CaseMetadata {
193 case_name: "lossy_rgba",
194 format: "VP8",
195 width: lossy_rgba_width,
196 height: lossy_rgba_height,
197 pixels_per_decode: lossy_rgba_width * lossy_rgba_height,
198 output_hash: lossy_rgba_hash,
199 },
200 || {
201 let image = decode_lossy_webp_to_rgba(black_box(lossy))
202 .map_err(|error| IoError::new(ErrorKind::InvalidData, error))?;
203 Ok(image.rgba.len())
204 },
205 )?,
206 measure(
207 &options,
208 CaseMetadata {
209 case_name: "lossy_yuv",
210 format: "VP8",
211 width: lossy_yuv.width,
212 height: lossy_yuv.height,
213 pixels_per_decode: lossy_yuv.width * lossy_yuv.height,
214 output_hash: lossy_yuv_hash,
215 },
216 || {
217 let image = decode_lossy_webp_to_yuv(black_box(lossy))
218 .map_err(|error| IoError::new(ErrorKind::InvalidData, error))?;
219 Ok(image.y.len() + image.u.len() + image.v.len())
220 },
221 )?,
222 measure(
223 &options,
224 CaseMetadata {
225 case_name: "lossless_rgba",
226 format: "VP8L",
227 width: lossless_width,
228 height: lossless_height,
229 pixels_per_decode: lossless_width * lossless_height,
230 output_hash: lossless_rgba_hash,
231 },
232 || {
233 let image = decode_lossless_webp_to_rgba(black_box(lossless))
234 .map_err(|error| IoError::new(ErrorKind::InvalidData, error))?;
235 Ok(image.rgba.len())
236 },
237 )?,
238 measure(
239 &options,
240 CaseMetadata {
241 case_name: "animation",
242 format: "ANIM",
243 width: animation_width,
244 height: animation_height,
245 pixels_per_decode: animation_pixels,
246 output_hash: animation_hash,
247 },
248 || {
249 let image = decode_animation_webp(black_box(animation))
250 .map_err(|error| IoError::new(ErrorKind::InvalidData, error))?;
251 Ok(image.frames.len())
252 },
253 )?,
254 ];
255
256 let mut csv = String::from(
257 "case,format,width,height,pixels_per_decode,output_hash,median_ns,p95_ns,mpixels_per_second\n",
258 );
259 for measurement in measurements {
260 let mpixels_per_second =
261 measurement.pixels_per_decode as f64 * 1_000.0 / measurement.median_ns;
262 csv.push_str(&format!(
263 "{},{},{},{},{},{:016x},{:.0},{:.0},{:.3}\n",
264 measurement.case_name,
265 measurement.format,
266 measurement.width,
267 measurement.height,
268 measurement.pixels_per_decode,
269 measurement.output_hash,
270 measurement.median_ns,
271 measurement.p95_ns,
272 mpixels_per_second,
273 ));
274 }
275 print!("{csv}");
276 if let Some(path) = options.output {
277 if let Some(parent) = path.parent() {
278 fs::create_dir_all(parent)?;
279 }
280 fs::write(path, csv)?;
281 }
282 Ok(())
283}