use std::env;
use std::hint::black_box;
use std::ptr;
use std::time::Instant;
use cityjson_lib::CityModel;
use cityjson_lib::ops;
use cityjson_lib_ffi_core::exports::{
cj_bytes_free, cj_bytes_list_free, cj_geometry_types_free, cj_model_append_model,
cj_model_clear_cityobject_geometry, cj_model_create, cj_model_free,
cj_model_parse_document_bytes, cj_model_serialize_document, cj_model_set_transform,
};
use cityjson_lib_ffi_core::{
cj_bytes_list_t, cj_bytes_t, cj_geometry_type_t, cj_geometry_types_t, cj_model_t,
cj_model_type_t, cj_status_t, cj_string_view_t, cj_transform_t,
};
const SMALL_FIXTURE: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/v2_0/minimal.city.json"
));
const LARGE_FIXTURE: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/v2_0/cityjson_fake_complete.city.json"
));
const APPEND_SOURCE_FIXTURE: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/v2_0/minimal.city.json"
));
#[derive(Clone, Copy)]
struct Fixture {
name: &'static str,
bytes: &'static [u8],
}
#[derive(Clone, Copy)]
struct Mode {
label: &'static str,
iterations: usize,
repeats: usize,
append_repeats: usize,
}
#[derive(Clone)]
struct Timing {
elapsed_ns: u128,
elapsed_per_iteration_ns: f64,
}
fn main() {
let quick = env::args().any(|arg| arg == "--quick");
let mode = if quick {
Mode {
label: "quick",
iterations: 250,
repeats: 7,
append_repeats: 11,
}
} else {
Mode {
label: "full",
iterations: 1_000,
repeats: 9,
append_repeats: 15,
}
};
let fixtures = [
Fixture {
name: "small",
bytes: SMALL_FIXTURE,
},
Fixture {
name: "medium",
bytes: LARGE_FIXTURE,
},
];
let results = fixtures
.iter()
.flat_map(|fixture| benchmark_fixture(*fixture, mode))
.chain(benchmark_append(mode))
.collect::<Vec<_>>();
let output = serde_json::json!({
"language": "rust",
"mode": mode.label,
"results": results,
});
println!("{}", serde_json::to_string_pretty(&output).unwrap());
}
#[allow(clippy::too_many_lines)]
fn benchmark_fixture(fixture: Fixture, mode: Mode) -> Vec<serde_json::Value> {
let direct_parse = measure_iterations(mode.iterations, mode.repeats, || {
black_box(cityjson_lib::json::from_slice(black_box(fixture.bytes)).unwrap())
});
let abi_parse = measure_iterations(mode.iterations, mode.repeats, || {
let handle = parse_document_abi(black_box(fixture.bytes));
assert_eq!(cj_model_free(handle), cj_status_t::CJ_STATUS_SUCCESS);
1usize
});
let direct_model = cityjson_lib::json::from_slice(fixture.bytes).unwrap();
let abi_model = parse_document_abi(fixture.bytes);
let direct_serialize = measure_iterations(mode.iterations, mode.repeats, || {
black_box(
cityjson_lib::json::to_vec_raw(
black_box(&direct_model),
&cityjson_lib::json::JsonWriteOptions::default(),
)
.unwrap(),
)
});
let abi_serialize = measure_iterations(mode.iterations, mode.repeats, || {
let mut payload = cj_bytes_t::null();
assert_eq!(
cj_model_serialize_document(black_box(abi_model), &raw mut payload),
cj_status_t::CJ_STATUS_SUCCESS
);
black_box(take_bytes(payload))
});
let direct_cityobject_ids = measure_iterations(mode.iterations, mode.repeats, || {
black_box(cityobject_ids_direct(black_box(&direct_model)))
});
let abi_cityobject_ids = measure_iterations(mode.iterations, mode.repeats, || {
black_box(cityobject_ids_abi(black_box(abi_model)))
});
let direct_geometry_types = measure_iterations(mode.iterations, mode.repeats, || {
black_box(geometry_types_direct(black_box(&direct_model)))
});
let abi_geometry_types = measure_iterations(mode.iterations, mode.repeats, || {
black_box(geometry_types_abi(black_box(abi_model)))
});
assert_eq!(cj_model_free(abi_model), cj_status_t::CJ_STATUS_SUCCESS);
black_box(direct_model);
vec![
result_json(
fixture.name,
mode,
"parse",
"direct",
mode.iterations,
mode.repeats,
&direct_parse,
),
result_json(
fixture.name,
mode,
"parse",
"abi",
mode.iterations,
mode.repeats,
&abi_parse,
),
result_json(
fixture.name,
mode,
"serialize",
"direct",
mode.iterations,
mode.repeats,
&direct_serialize,
),
result_json(
fixture.name,
mode,
"serialize",
"abi",
mode.iterations,
mode.repeats,
&abi_serialize,
),
result_json(
fixture.name,
mode,
"cityobject_ids",
"direct",
mode.iterations,
mode.repeats,
&direct_cityobject_ids,
),
result_json(
fixture.name,
mode,
"cityobject_ids",
"abi",
mode.iterations,
mode.repeats,
&abi_cityobject_ids,
),
result_json(
fixture.name,
mode,
"geometry_types",
"direct",
mode.iterations,
mode.repeats,
&direct_geometry_types,
),
result_json(
fixture.name,
mode,
"geometry_types",
"abi",
mode.iterations,
mode.repeats,
&abi_geometry_types,
),
]
}
fn benchmark_append(mode: Mode) -> Vec<serde_json::Value> {
let source = Fixture {
name: "empty<-small",
bytes: APPEND_SOURCE_FIXTURE,
};
let direct_append = measure_append_direct(mode.append_repeats, || {
let mut target =
cityjson_lib::CityModel::new(cityjson_lib::cityjson_types::CityModelType::CityJSON);
apply_unit_transform(&mut target);
let mut source_model = cityjson_lib::json::from_slice(source.bytes).unwrap();
apply_unit_transform(&mut source_model);
clear_cityobject_geometries_direct(&mut source_model);
(target, source_model)
});
let abi_append = measure_append_abi(mode.append_repeats, || {
let mut target = ptr::null_mut();
assert_eq!(
cj_model_create(cj_model_type_t::CJ_MODEL_TYPE_CITY_JSON, &raw mut target),
cj_status_t::CJ_STATUS_SUCCESS
);
assert_eq!(
cj_model_set_transform(target, unit_transform()),
cj_status_t::CJ_STATUS_SUCCESS
);
let source_handle = parse_document_abi(source.bytes);
assert_eq!(
cj_model_set_transform(source_handle, unit_transform()),
cj_status_t::CJ_STATUS_SUCCESS
);
clear_cityobject_geometries_abi(source_handle);
(target, source_handle)
});
vec![
result_json(
source.name,
mode,
"append",
"direct",
1,
mode.append_repeats,
&direct_append,
),
result_json(
source.name,
mode,
"append",
"abi",
1,
mode.append_repeats,
&abi_append,
),
]
}
fn measure_iterations<T, F>(iterations: usize, repeats: usize, mut action: F) -> Timing
where
F: FnMut() -> T,
{
let mut samples = Vec::with_capacity(repeats);
for _ in 0..repeats {
let started = Instant::now();
for _ in 0..iterations {
black_box(action());
}
samples.push(started.elapsed().as_nanos());
}
summarize_samples(samples, iterations)
}
fn measure_append_direct<F>(repeats: usize, mut setup: F) -> Timing
where
F: FnMut() -> (CityModel, CityModel),
{
let mut samples = Vec::with_capacity(repeats);
for _ in 0..repeats {
let (mut target, source) = setup();
let started = Instant::now();
ops::append(&mut target, &source).unwrap();
samples.push(started.elapsed().as_nanos());
black_box(target);
black_box(source);
}
summarize_samples(samples, 1)
}
fn measure_append_abi<F>(repeats: usize, mut setup: F) -> Timing
where
F: FnMut() -> (*mut cj_model_t, *mut cj_model_t),
{
let mut samples = Vec::with_capacity(repeats);
for _ in 0..repeats {
let (target, source) = setup();
let started = Instant::now();
assert_eq!(
cj_model_append_model(target, source),
cj_status_t::CJ_STATUS_SUCCESS
);
samples.push(started.elapsed().as_nanos());
assert_eq!(cj_model_free(target), cj_status_t::CJ_STATUS_SUCCESS);
assert_eq!(cj_model_free(source), cj_status_t::CJ_STATUS_SUCCESS);
}
summarize_samples(samples, 1)
}
#[allow(clippy::cast_precision_loss)]
fn summarize_samples(samples: Vec<u128>, iterations: usize) -> Timing {
let elapsed_ns = median(samples);
Timing {
elapsed_ns,
elapsed_per_iteration_ns: elapsed_ns as f64 / iterations as f64,
}
}
fn median(mut samples: Vec<u128>) -> u128 {
samples.sort_unstable();
if samples.is_empty() {
return 0;
}
if samples.len() % 2 == 1 {
return samples[samples.len() / 2];
}
let upper = samples[samples.len() / 2];
let lower = samples[(samples.len() / 2) - 1];
u128::midpoint(upper, lower)
}
fn result_json(
fixture: &'static str,
mode: Mode,
operation: &'static str,
layer: &'static str,
iterations: usize,
repeats: usize,
timing: &Timing,
) -> serde_json::Value {
serde_json::json!({
"fixture": fixture,
"mode": mode.label,
"operation": operation,
"layer": layer,
"iterations": iterations,
"repeats": repeats,
"elapsed_ns": timing.elapsed_ns,
"elapsed_per_iteration_ns": timing.elapsed_per_iteration_ns,
})
}
fn parse_document_abi(bytes: &[u8]) -> *mut cj_model_t {
let mut handle = ptr::null_mut();
assert_eq!(
cj_model_parse_document_bytes(bytes.as_ptr(), bytes.len(), &raw mut handle),
cj_status_t::CJ_STATUS_SUCCESS
);
handle
}
fn cityobject_ids_direct(model: &CityModel) -> Vec<String> {
model
.cityobjects()
.iter()
.map(|(_, cityobject)| cityobject.id().to_owned())
.collect()
}
fn cityobject_ids_abi(model: *const cj_model_t) -> Vec<String> {
let mut payload = cj_bytes_list_t::null();
assert_eq!(
cityjson_lib_ffi_core::exports::cj_model_copy_cityobject_ids(model, &raw mut payload),
cj_status_t::CJ_STATUS_SUCCESS
);
take_bytes_list(payload)
}
fn geometry_types_direct(model: &CityModel) -> Vec<cj_geometry_type_t> {
model
.iter_geometries()
.map(|(_, geometry)| (*geometry.type_geometry()).into())
.collect()
}
fn geometry_types_abi(model: *const cj_model_t) -> Vec<cj_geometry_type_t> {
let mut payload = cj_geometry_types_t::null();
assert_eq!(
cityjson_lib_ffi_core::exports::cj_model_copy_geometry_types(model, &raw mut payload),
cj_status_t::CJ_STATUS_SUCCESS
);
take_geometry_types(payload)
}
fn take_bytes(bytes: cj_bytes_t) -> usize {
let len = bytes.len;
if len > 0 {
assert!(!bytes.data.is_null());
unsafe {
let _ = std::slice::from_raw_parts(bytes.data.cast_const(), len);
}
}
assert_eq!(cj_bytes_free(bytes), cj_status_t::CJ_STATUS_SUCCESS);
len
}
fn take_bytes_list(bytes: cj_bytes_list_t) -> Vec<String> {
if bytes.len == 0 {
assert_eq!(cj_bytes_list_free(bytes), cj_status_t::CJ_STATUS_SUCCESS);
return Vec::new();
}
assert!(!bytes.data.is_null());
let mut values = Vec::with_capacity(bytes.len);
for index in 0..bytes.len {
let item = unsafe { *bytes.data.add(index) };
if item.len == 0 {
values.push(String::new());
continue;
}
let bytes = unsafe { std::slice::from_raw_parts(item.data.cast_const(), item.len) };
values.push(std::str::from_utf8(bytes).unwrap().to_owned());
}
assert_eq!(cj_bytes_list_free(bytes), cj_status_t::CJ_STATUS_SUCCESS);
values
}
fn take_geometry_types(types: cj_geometry_types_t) -> Vec<cj_geometry_type_t> {
if types.len == 0 {
assert_eq!(
cj_geometry_types_free(types),
cj_status_t::CJ_STATUS_SUCCESS
);
return Vec::new();
}
assert!(!types.data.is_null());
let values = unsafe { std::slice::from_raw_parts(types.data.cast_const(), types.len) }.to_vec();
assert_eq!(
cj_geometry_types_free(types),
cj_status_t::CJ_STATUS_SUCCESS
);
values
}
fn clear_cityobject_geometries_direct(model: &mut cityjson_lib::CityModel) {
for (_, cityobject) in model.cityobjects_mut().iter_mut() {
cityobject.clear_geometry();
}
}
fn clear_cityobject_geometries_abi(model: *mut cj_model_t) {
let ids = cityobject_ids_abi(model.cast_const());
for id in ids {
let view = cj_string_view_t {
data: id.as_bytes().as_ptr(),
len: id.len(),
};
assert_eq!(
cj_model_clear_cityobject_geometry(model, view),
cj_status_t::CJ_STATUS_SUCCESS
);
}
}
fn unit_transform() -> cj_transform_t {
cj_transform_t {
scale_x: 1.0,
scale_y: 1.0,
scale_z: 1.0,
translate_x: 0.0,
translate_y: 0.0,
translate_z: 0.0,
}
}
fn apply_unit_transform(model: &mut cityjson_lib::CityModel) {
let transform = model.transform_mut();
transform.set_scale([1.0, 1.0, 1.0]);
transform.set_translate([0.0, 0.0, 0.0]);
}