Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
run: zig build test
- name: Run lookup example
run: zig build example_lookup
- name: Run within example
run: zig build example_within
- name: Run scan example
run: zig build example_scan
- name: Run inspect example
run: zig build example_inspect
165 changes: 154 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,37 @@ var db = try maxminddb.Reader.mmap(allocator, db_path, .{ .ipv4_index_first_n_bi
defer db.close();
```

Use `ArenaAllocator` for best performance, see [benchmarks](./benchmarks/).
Each `lookup` result owns an arena with all decoded allocations.
Call `deinit()` to free it or use `ArenaAllocator` with `reset()`,
see [benchmarks](./benchmarks/lookup.zig).

```zig
if (try db.lookup(maxminddb.geolite2.City, allocator, ip, .{})) |result| {
defer result.deinit();
std.debug.print("{f} {s}\n", .{ result.network, result.value.city.names.?.get("en").? });
}

var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();

const arena_allocator = arena.allocator();
for (ips) |ip| {
if (try db.lookup(maxminddb.geolite2.City, arena_allocator, ip, .{})) |result| {
std.debug.print("{f} {s}\n", .{ result.network, result.value.city.names.?.get("en").? });
}

_ = arena.reset(.retain_capacity);
}
```

If you don't need all the fields, use `.only` to decode only the top-level fields you want.

```zig
const fields = &.{ "city", "country" };
const city = try db.lookup(allocator, maxminddb.geolite2.City, ip, .{ .only = fields });
if (try db.lookup(maxminddb.geolite2.City, allocator, ip, .{ .only = fields })) |result| {
defer result.deinit();
std.debug.print("{f} {s}\n", .{ result.network, result.value.city.names.?.get("en").? });
}
```

Alternatively, define your own struct with only the fields you need.
Expand All @@ -74,27 +98,42 @@ const MyCity = struct {
} = .{},
};

const city = try db.lookup(allocator, MyCity, ip, .{});
if (try db.lookup(MyCity, allocator, ip, .{})) |result| {
defer result.deinit();
std.debug.print("{s}\n", .{result.value.city.names.en});
}
```

Use `any.Value` to decode any record without knowing the schema.

```zig
const result = try db.lookup(allocator, maxminddb.any.Value, ip, .{ .only = fields });
if (result) |r| {
if (try db.lookup(maxminddb.any.Value, allocator, ip, .{ .only = fields })) |result| {
defer result.deinit();
// Formats as compact JSON.
std.debug.print("{f}\n", .{r.value});
std.debug.print("{f}\n", .{result.value});
}
```

Use `lookupWithCache` to skip decoding when different IPs resolve to the same record.
The cache owns decoded memory, so results don't need to be individually freed.

```zig
var cache = try maxminddb.Cache(maxminddb.geolite2.City).init(allocator, .{});
defer cache.deinit();

if (try db.lookupWithCache(maxminddb.geolite2.City, &cache, ip, .{})) |result| {
std.debug.print("{f} {s}\n", .{ result.network, result.value.city.names.?.get("en").? });
}
```

Here are reference results on Apple M2 Pro (1M random IPv4 lookups against GeoLite2-City
with `ipv4_index_first_n_bits = 16`):

| Benchmark | All fields | Filtered (city) |
|--- |--- |--- |
| `geolite2.City` | ~1,284,000 | ~1,348,000 |
| `MyCity` | ~1,383,000 | |
| `any.Value` | ~1,254,000 | ~1,349,000 |
| Type | Default | `.only` | `Cache` |
|--- |--- |--- |--- |
| `geolite2.City` | ~1,420,000 | ~1,348,000 | ~1,565,000 |
| `MyCity` | ~1,383,000 | | |
| `any.Value` | ~1,254,000 | ~1,349,000 | |

<details>

Expand Down Expand Up @@ -140,6 +179,30 @@ Lookups Per Second (avg):1315870.3443053183

<details>

<summary>geolite2.City with Cache</summary>

```sh
$ for i in $(seq 1 10); do
zig build benchmark_lookup_cache -Doptimize=ReleaseFast -- GeoLite2-City.mmdb 1000000 \
2>&1 | grep 'Lookups Per Second'
done

Lookups Per Second (avg):1493822.3908664712
Lookups Per Second (avg):1503051.0049070602
Lookups Per Second (avg):1499514.437731375
Lookups Per Second (avg):1491749.9700251492
Lookups Per Second (avg):1449924.9391983037
Lookups Per Second (avg):1396100.6211600688
Lookups Per Second (avg):1465750.9875955326
Lookups Per Second (avg):1515611.9396877384
Lookups Per Second (avg):1485235.6423035355
Lookups Per Second (avg):1439334.222943596
```

</details>

<details>

<summary>MyCity</summary>

```sh
Expand Down Expand Up @@ -203,3 +266,83 @@ Lookups Per Second (avg):1315986.2950186788
```

</details>

Use `scan` to iterate over all networks in the database.

```zig
var it = try db.scan(maxminddb.any.Value, allocator, maxminddb.Network.all_ipv6, .{});

while (try it.next()) |item| {
defer item.deinit();
std.debug.print("{f} {f}\n", .{ item.network, item.value });
}
```

Use `scanWithCache` to avoid re-decoding networks that share the same record.
The cache owns decoded memory, so results don't need to be individually freed.

```zig
var cache = try maxminddb.Cache(maxminddb.any.Value).init(allocator, .{});
defer cache.deinit();

var it = try db.scanWithCache(maxminddb.any.Value, &cache, maxminddb.Network.all_ipv6, .{});

while (try it.next()) |item| {
std.debug.print("{f} {f}\n", .{ item.network, item.value });
}
```

Here are reference results on Apple M2 Pro (full GeoLite2-City scan using `any.Value`):

| Mode | Records/sec |
|--- |--- |
| Default | ~1,295,000 |
| `Cache` | ~2,930,000 |

<details>

<summary>no cache (any.Value)</summary>

```sh
$ for i in $(seq 1 10); do
zig build benchmark_scan -Doptimize=ReleaseFast -- GeoLite2-City.mmdb \
2>&1 | grep 'Records Per Second'
done

Records Per Second: 1216758.945145436
Records Per Second: 1238440.9772222256
Records Per Second: 1234710.6362391203
Records Per Second: 1229527.4688849829
Records Per Second: 1243478.3908140333
Records Per Second: 1226863.3718734735
Records Per Second: 1240073.3248202254
Records Per Second: 1247541.1528026997
Records Per Second: 1230510.441029532
Records Per Second: 1246311.587919839
```

</details>

<details>

<summary>cache (any.Value)</summary>

```sh
$ for i in $(seq 1 10); do
zig build benchmark_scan_cache -Doptimize=ReleaseFast -- GeoLite2-City.mmdb \
2>&1 | grep 'Records Per Second'
done

Records Per Second: 2847560.3756875996
Records Per Second: 2925388.867798729
Records Per Second: 2919203.9046571665
Records Per Second: 2814410.555872645
Records Per Second: 2933972.04386147
Records Per Second: 2900700.06160036
Records Per Second: 2922279.338699886
Records Per Second: 2862525.847598088
Records Per Second: 2916760.542913819
Records Per Second: 2908245.98918392
```

</details>
2 changes: 1 addition & 1 deletion benchmarks/inspect.zig
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ pub fn main() !void {
const ip = std.net.Address.initIp4(ip_bytes, 0);

const result = db.lookup(
arena_allocator,
maxminddb.any.Value,
arena_allocator,
ip,
.{ .only = fields },
) catch |err| {
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/lookup.zig
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ pub fn main() !void {
const ip = std.net.Address.initIp4(ip_bytes, 0);

const result = db.lookup(
arena_allocator,
maxminddb.geolite2.City,
arena_allocator,
ip,
.{ .only = fields },
) catch |err| {
Expand Down
91 changes: 91 additions & 0 deletions benchmarks/lookup_cache.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const std = @import("std");
const maxminddb = @import("maxminddb");

const default_db_path: []const u8 = "GeoLite2-City.mmdb";
const default_num_lookups: u64 = 1_000_000;
const max_mmdb_fields = 32;

pub fn main() !void {
const allocator = std.heap.smp_allocator;

const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);

var db_path: []const u8 = default_db_path;
var num_lookups = default_num_lookups;
var fields: ?[]const []const u8 = null;
if (args.len > 1) db_path = args[1];
if (args.len > 2) num_lookups = try std.fmt.parseUnsigned(u64, args[2], 10);
if (args.len > 3) {
var items: [max_mmdb_fields][]const u8 = undefined;

var it = std.mem.splitScalar(u8, args[3], ',');
var i: usize = 0;
while (it.next()) |part| : (i += 1) {
items[i] = part;
}

fields = items[0..i];
}

std.debug.print("Benchmarking with:\n", .{});
std.debug.print(" Database: {s}\n", .{db_path});
std.debug.print(" Lookups: {d}\n", .{num_lookups});
std.debug.print("Opening database...\n", .{});

var open_timer = try std.time.Timer.start();
var db = try maxminddb.Reader.mmap(allocator, db_path, .{ .ipv4_index_first_n_bits = 16 });
defer db.close();
const open_time_ms = @as(f64, @floatFromInt(open_timer.read())) /
@as(f64, @floatFromInt(std.time.ns_per_ms));
std.debug.print("Database opened successfully in {d} ms. Type: {s}\n", .{
open_time_ms,
db.metadata.database_type,
});

var cache = try maxminddb.Cache(maxminddb.geolite2.City).init(allocator, .{});
defer cache.deinit();

std.debug.print("Starting benchmark...\n", .{});
var timer = try std.time.Timer.start();
var not_found_count: u64 = 0;
var lookup_errors: u64 = 0;
var ip_bytes: [4]u8 = undefined;

for (0..num_lookups) |_| {
std.crypto.random.bytes(&ip_bytes);
const ip = std.net.Address.initIp4(ip_bytes, 0);

const result = db.lookupWithCache(
maxminddb.geolite2.City,
&cache,
ip,
.{ .only = fields },
) catch |err| {
std.debug.print("! Lookup error for IP {any}: {any}\n", .{ ip, err });
lookup_errors += 1;
continue;
};
if (result == null) {
not_found_count += 1;
continue;
}
}

const elapsed_ns = timer.read();
const elapsed_s = @as(f64, @floatFromInt(elapsed_ns)) /
@as(f64, @floatFromInt(std.time.ns_per_s));
const lookups_per_second = if (elapsed_s > 0)
@as(f64, @floatFromInt(num_lookups)) / elapsed_s
else
0.0;
const successful_lookups = num_lookups - not_found_count - lookup_errors;

std.debug.print("\n--- Benchmark Finished ---\n", .{});
std.debug.print("Total Lookups Attempted: {d}\n", .{num_lookups});
std.debug.print("Successful Lookups: {d}\n", .{successful_lookups});
std.debug.print("IPs Not Found: {d}\n", .{not_found_count});
std.debug.print("Lookup Errors: {d}\n", .{lookup_errors});
std.debug.print("Elapsed Time: {d} s\n", .{elapsed_s});
std.debug.print("Lookups Per Second (avg):{d}\n", .{lookups_per_second});
}
2 changes: 1 addition & 1 deletion benchmarks/mycity.zig
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ pub fn main() !void {
const ip = std.net.Address.initIp4(ip_bytes, 0);

const result = db.lookup(
arena_allocator,
MyCity,
arena_allocator,
ip,
.{},
) catch |err| {
Expand Down
58 changes: 58 additions & 0 deletions benchmarks/scan.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const std = @import("std");
const maxminddb = @import("maxminddb");

const default_db_path: []const u8 = "GeoLite2-City.mmdb";

pub fn main() !void {
const allocator = std.heap.smp_allocator;

const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);

var db_path: []const u8 = default_db_path;
if (args.len > 1) db_path = args[1];

std.debug.print("Benchmarking with:\n", .{});
std.debug.print(" Database: {s}\n", .{db_path});
std.debug.print("Opening database...\n", .{});

var open_timer = try std.time.Timer.start();
var db = try maxminddb.Reader.mmap(allocator, db_path, .{});
defer db.close();
const open_time_ms = @as(f64, @floatFromInt(open_timer.read())) /
@as(f64, @floatFromInt(std.time.ns_per_ms));
std.debug.print("Database opened successfully in {d} ms. Type: {s}\n", .{
open_time_ms,
db.metadata.database_type,
});

const network = if (db.metadata.ip_version == 4)
maxminddb.Network.all_ipv4
else
maxminddb.Network.all_ipv6;

std.debug.print("Starting benchmark...\n", .{});
var timer = try std.time.Timer.start();

var it = try db.scan(maxminddb.any.Value, allocator, network, .{});

var n: usize = 0;
while (try it.next()) |item| {
n += 1;
item.deinit();
}

const elapsed_ns = timer.read();
const elapsed_s = @as(f64, @floatFromInt(elapsed_ns)) /
@as(f64, @floatFromInt(std.time.ns_per_s));

const records_per_second = if (elapsed_s > 0)
@as(f64, @floatFromInt(n)) / elapsed_s
else
0.0;

std.debug.print("\n--- Benchmark Finished ---\n", .{});
std.debug.print("Records: {d}\n", .{n});
std.debug.print("Elapsed Time: {d} s\n", .{elapsed_s});
std.debug.print("Records Per Second: {d}\n", .{records_per_second});
}
Loading
Loading