tg_geometry 0.3.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/benchmark/_support.rb +450 -5
- data/benchmark/batch_packed_vs_loop.rb +38 -6
- data/benchmark/ewkb_roundtrip.rb +18 -9
- data/benchmark/falcon_concurrency.rb +17 -10
- data/benchmark/feature_source.rb +49 -23
- data/benchmark/flat_vs_rtree.rb +53 -11
- data/benchmark/geom_query.rb +19 -9
- data/benchmark/gvl_threshold.rb +10 -11
- data/benchmark/nearest_segment.rb +10 -6
- data/benchmark/objectspace_memsize.rb +14 -1
- data/benchmark/parse_throughput.rb +13 -10
- data/benchmark/rss_stability.rb +35 -24
- data/ext/tg_geometry/tg_geometry_ext.c +53 -18
- data/lib/tg/geometry/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cde617c534a8a3d9702b3fe212d46f966da46224499bb4fcb97bf40865590be8
|
|
4
|
+
data.tar.gz: 7e1767d31814249ef695ceae1965d6c5aaf9065ff7595309e95698abc053abbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2cf6070d21ca5342425600e436523aae7215a2792d2d8392662a6c5257ba4e178301ff2a93c5ebb847fc7606d7b1aad60b9ab1fcd8e21b67f06d5f72f8c3cfa1
|
|
7
|
+
data.tar.gz: 99a9f9c086c6a2da9add012f57a99de800f67846007503ddf6c5d63ec25ac2a6d62cfe5b854fe1d44976ff0bddcbce04b8831db631dd8d174602c64914190e30
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 - 28.05.2026
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Optimized R-tree-backed point lookup by removing per-query candidate bitmap
|
|
7
|
+
allocation and full entry scan from Index#find_covering. The new path tracks
|
|
8
|
+
the lowest matching ordinal directly in the R-tree callback, preserving
|
|
9
|
+
insertion-order semantics while significantly improving rtree point queries
|
|
10
|
+
and packed batch point lookup.
|
|
11
|
+
|
|
3
12
|
## 0.3.0 - 27.05.2026
|
|
4
13
|
|
|
5
14
|
### Added
|
data/benchmark/_support.rb
CHANGED
|
@@ -21,14 +21,447 @@ module TGGeometryBench
|
|
|
21
21
|
module_function
|
|
22
22
|
|
|
23
23
|
SIZES = [100, 500, 1_000, 5_000, 50_000].freeze
|
|
24
|
-
FAST_SIZES = [100,
|
|
24
|
+
FAST_SIZES = [100, 1_000, 5_000].freeze
|
|
25
|
+
|
|
26
|
+
DEFAULT_REPEATS = 5
|
|
27
|
+
DEFAULT_MIN_SECONDS = 0.25
|
|
28
|
+
DEFAULT_WARMUP_SECONDS = 0.05
|
|
29
|
+
DEFAULT_MAX_ITERATIONS = 100_000_000
|
|
30
|
+
|
|
31
|
+
def full?
|
|
32
|
+
ENV["TGEOMETRY_BENCH_FULL"] == "1"
|
|
33
|
+
end
|
|
25
34
|
|
|
26
35
|
def sizes
|
|
27
|
-
|
|
36
|
+
full? ? SIZES : FAST_SIZES
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def env_integer(name, default, min: nil)
|
|
40
|
+
value = Integer(ENV.fetch(name, default.to_s))
|
|
41
|
+
if min && value < min
|
|
42
|
+
raise ArgumentError, "#{name} must be >= #{min}, got #{value}"
|
|
43
|
+
end
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def env_float(name, default, min: nil)
|
|
48
|
+
value = Float(ENV.fetch(name, default.to_s))
|
|
49
|
+
if min && value < min
|
|
50
|
+
raise ArgumentError, "#{name} must be >= #{min}, got #{value}"
|
|
51
|
+
end
|
|
52
|
+
value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initial_iterations(default)
|
|
56
|
+
env_integer("TGEOMETRY_BENCH_ITERATIONS", default, min: 1)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def repeats(default = DEFAULT_REPEATS)
|
|
60
|
+
env_integer("TGEOMETRY_BENCH_REPEATS", default, min: 1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def min_seconds(default = DEFAULT_MIN_SECONDS)
|
|
64
|
+
env_float("TGEOMETRY_BENCH_MIN_SECONDS", default, min: 0.0)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def warmup_seconds(default = DEFAULT_WARMUP_SECONDS)
|
|
68
|
+
env_float("TGEOMETRY_BENCH_WARMUP_SECONDS", default, min: 0.0)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def max_iterations(default = DEFAULT_MAX_ITERATIONS)
|
|
72
|
+
env_integer("TGEOMETRY_BENCH_MAX_ITERATIONS", default, min: 1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def adaptive?
|
|
76
|
+
ENV.fetch("TGEOMETRY_BENCH_ADAPTIVE", "1") != "0"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def disable_gc_during_timed?
|
|
80
|
+
ENV["TGEOMETRY_BENCH_DISABLE_GC"] == "1"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def monotonic
|
|
84
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def median(values)
|
|
88
|
+
sorted = values.sort
|
|
89
|
+
n = sorted.length
|
|
90
|
+
return 0.0 if n.zero?
|
|
91
|
+
|
|
92
|
+
mid = n / 2
|
|
93
|
+
n.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def percentile(values, pct)
|
|
97
|
+
sorted = values.sort
|
|
98
|
+
return 0.0 if sorted.empty?
|
|
99
|
+
|
|
100
|
+
rank = (pct * (sorted.length - 1)).round
|
|
101
|
+
sorted.fetch(rank)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def gc_start
|
|
105
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
|
106
|
+
rescue ArgumentError
|
|
107
|
+
GC.start
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def timed_gc_delta
|
|
111
|
+
before = GC.stat
|
|
112
|
+
seconds = nil
|
|
113
|
+
|
|
114
|
+
if disable_gc_during_timed?
|
|
115
|
+
was_enabled = GC.enable
|
|
116
|
+
GC.disable
|
|
117
|
+
seconds = Benchmark.realtime { yield }
|
|
118
|
+
GC.enable if was_enabled
|
|
119
|
+
else
|
|
120
|
+
seconds = Benchmark.realtime { yield }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
after = GC.stat
|
|
124
|
+
[
|
|
125
|
+
seconds,
|
|
126
|
+
after[:total_allocated_objects] - before[:total_allocated_objects],
|
|
127
|
+
after[:minor_gc_count] - before[:minor_gc_count],
|
|
128
|
+
after[:major_gc_count] - before[:major_gc_count]
|
|
129
|
+
]
|
|
130
|
+
ensure
|
|
131
|
+
GC.enable if disable_gc_during_timed?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def calibrate_iterations(initial_iterations:, min_seconds:, max_iterations: self.max_iterations, &block)
|
|
135
|
+
iterations = initial_iterations
|
|
136
|
+
return iterations unless adaptive?
|
|
137
|
+
return iterations if min_seconds <= 0.0
|
|
138
|
+
|
|
139
|
+
loop do
|
|
140
|
+
gc_start
|
|
141
|
+
seconds = Benchmark.realtime { block.call(iterations) }
|
|
142
|
+
break iterations if seconds >= min_seconds || iterations >= max_iterations
|
|
143
|
+
|
|
144
|
+
if seconds <= 0.0
|
|
145
|
+
iterations = [iterations * 10, max_iterations].min
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
target = (iterations * (min_seconds / seconds) * 1.50).ceil
|
|
150
|
+
iterations = [[target, iterations * 2].max, max_iterations].min
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def warmup_for(seconds, iterations, &block)
|
|
155
|
+
return if seconds <= 0.0
|
|
156
|
+
|
|
157
|
+
deadline = monotonic + seconds
|
|
158
|
+
begin
|
|
159
|
+
block.call(iterations)
|
|
160
|
+
end while monotonic < deadline
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def measure_counted(initial_iterations:, operations_per_iteration: 1, min_seconds: self.min_seconds,
|
|
164
|
+
repeats: self.repeats, warmup_seconds: self.warmup_seconds,
|
|
165
|
+
max_iterations: self.max_iterations, &block)
|
|
166
|
+
iterations = calibrate_iterations(
|
|
167
|
+
initial_iterations: initial_iterations,
|
|
168
|
+
min_seconds: min_seconds,
|
|
169
|
+
max_iterations: max_iterations,
|
|
170
|
+
&block
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
warmup_for(warmup_seconds, iterations, &block)
|
|
174
|
+
|
|
175
|
+
seconds_samples = []
|
|
176
|
+
allocation_samples = []
|
|
177
|
+
minor_gc_samples = []
|
|
178
|
+
major_gc_samples = []
|
|
179
|
+
|
|
180
|
+
repeats.times do
|
|
181
|
+
gc_start
|
|
182
|
+
seconds, allocations, minor_gc, major_gc = timed_gc_delta { block.call(iterations) }
|
|
183
|
+
seconds_samples << seconds
|
|
184
|
+
allocation_samples << allocations
|
|
185
|
+
minor_gc_samples << minor_gc
|
|
186
|
+
major_gc_samples << major_gc
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
total_operations = iterations * operations_per_iteration
|
|
190
|
+
median_sec = median(seconds_samples)
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
iterations: iterations,
|
|
194
|
+
operations_per_iteration: operations_per_iteration,
|
|
195
|
+
operations_per_repeat: total_operations,
|
|
196
|
+
repeats: repeats,
|
|
197
|
+
min_seconds: min_seconds,
|
|
198
|
+
warmup_seconds: warmup_seconds,
|
|
199
|
+
best_sec: seconds_samples.min,
|
|
200
|
+
median_sec: median_sec,
|
|
201
|
+
worst_sec: seconds_samples.max,
|
|
202
|
+
mean_sec: seconds_samples.sum / seconds_samples.length,
|
|
203
|
+
p90_sec: percentile(seconds_samples, 0.90),
|
|
204
|
+
ops_per_sec: median_sec.positive? ? total_operations / median_sec : 0.0,
|
|
205
|
+
ns_per_op: median_sec.positive? ? (median_sec / total_operations) * 1e9 : 0.0,
|
|
206
|
+
spread_pct: seconds_samples.min.positive? ? ((seconds_samples.max - seconds_samples.min) / seconds_samples.min) * 100.0 : 0.0,
|
|
207
|
+
median_allocations: median(allocation_samples),
|
|
208
|
+
allocations_per_op: total_operations.positive? ? median(allocation_samples) / total_operations.to_f : 0.0,
|
|
209
|
+
median_minor_gc: median(minor_gc_samples),
|
|
210
|
+
median_major_gc: median(major_gc_samples),
|
|
211
|
+
samples_sec: seconds_samples
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def format_value(value)
|
|
216
|
+
case value
|
|
217
|
+
when Float
|
|
218
|
+
if value.finite?
|
|
219
|
+
"%.6f" % value
|
|
220
|
+
else
|
|
221
|
+
value.to_s
|
|
222
|
+
end
|
|
223
|
+
when Array
|
|
224
|
+
value.join(":")
|
|
225
|
+
else
|
|
226
|
+
value.to_s.gsub(/\s+/, "_")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def output_format
|
|
231
|
+
ENV.fetch("TGEOMETRY_BENCH_FORMAT", "table")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def kv_output?
|
|
235
|
+
%w[kv both].include?(output_format)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def table_output?
|
|
239
|
+
%w[table both].include?(output_format)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def report_rows
|
|
243
|
+
@report_rows ||= []
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def kv_line(payload)
|
|
247
|
+
payload.map { |key, value| "#{key}=#{format_value(value)}" }.join(" ")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def report(benchmark, fields = nil, stats: nil, **kwargs)
|
|
251
|
+
fields = (fields || {}).merge(kwargs)
|
|
252
|
+
|
|
253
|
+
payload = {
|
|
254
|
+
benchmark: benchmark,
|
|
255
|
+
ruby: RUBY_VERSION,
|
|
256
|
+
platform: RUBY_PLATFORM,
|
|
257
|
+
full: full?,
|
|
258
|
+
adaptive: adaptive?,
|
|
259
|
+
gc_disabled: disable_gc_during_timed?
|
|
260
|
+
}.merge(fields)
|
|
261
|
+
|
|
262
|
+
if stats
|
|
263
|
+
payload = payload.merge(
|
|
264
|
+
iterations: stats[:iterations],
|
|
265
|
+
ops_per_iteration: stats[:operations_per_iteration],
|
|
266
|
+
operations: stats[:operations_per_repeat],
|
|
267
|
+
repeats: stats[:repeats],
|
|
268
|
+
min_seconds: stats[:min_seconds],
|
|
269
|
+
median_sec: stats[:median_sec],
|
|
270
|
+
best_sec: stats[:best_sec],
|
|
271
|
+
worst_sec: stats[:worst_sec],
|
|
272
|
+
ops_per_sec: stats[:ops_per_sec],
|
|
273
|
+
ns_per_op: stats[:ns_per_op],
|
|
274
|
+
spread_pct: stats[:spread_pct],
|
|
275
|
+
median_allocations: stats[:median_allocations],
|
|
276
|
+
allocations_per_op: stats[:allocations_per_op],
|
|
277
|
+
median_minor_gc: stats[:median_minor_gc],
|
|
278
|
+
median_major_gc: stats[:median_major_gc]
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
puts kv_line(payload) if kv_output?
|
|
283
|
+
report_rows << payload if table_output?
|
|
284
|
+
payload
|
|
28
285
|
end
|
|
29
286
|
|
|
30
|
-
|
|
31
|
-
|
|
287
|
+
TABLE_CONTEXT_COLUMNS = [
|
|
288
|
+
:kind, :n, :strategy, :mode,
|
|
289
|
+
:query, :method, :case, :format,
|
|
290
|
+
:library, :operation,
|
|
291
|
+
:point_index, :lon, :lat,
|
|
292
|
+
:rect_index, :rect,
|
|
293
|
+
:segments, :target_bytes, :payload_bytes,
|
|
294
|
+
:points_per_batch, :threads,
|
|
295
|
+
:entries, :rebuilds, :cycle
|
|
296
|
+
].freeze
|
|
297
|
+
|
|
298
|
+
TABLE_METRIC_COLUMNS = [
|
|
299
|
+
:batches_per_sec, :ops_per_sec, :qps, :ns_per_op,
|
|
300
|
+
:median_sec, :spread_pct, :allocations_per_op,
|
|
301
|
+
:median_minor_gc, :median_major_gc,
|
|
302
|
+
:iterations, :operations,
|
|
303
|
+
:geom_memsize, :flat_memsize, :rtree_memsize, :rtree_over_flat,
|
|
304
|
+
:start_rss_kb, :peak_rss_kb, :finish_rss_kb, :drift_kb, :max_drift_kb,
|
|
305
|
+
:elapsed_sec, :queries, :sample_count, :rss_kb
|
|
306
|
+
].freeze
|
|
307
|
+
|
|
308
|
+
TABLE_INTERNAL_COLUMNS = [
|
|
309
|
+
:benchmark, :ruby, :platform, :full, :adaptive, :gc_disabled,
|
|
310
|
+
:repeats, :min_seconds, :warmup_seconds, :max_iterations,
|
|
311
|
+
:ops_per_iteration, :median_allocations,
|
|
312
|
+
:best_sec, :worst_sec
|
|
313
|
+
].freeze
|
|
314
|
+
|
|
315
|
+
TABLE_HEADERS = {
|
|
316
|
+
kind: "kind",
|
|
317
|
+
n: "n",
|
|
318
|
+
strategy: "strategy",
|
|
319
|
+
mode: "mode",
|
|
320
|
+
query: "query",
|
|
321
|
+
method: "method",
|
|
322
|
+
case: "case",
|
|
323
|
+
format: "format",
|
|
324
|
+
library: "lib",
|
|
325
|
+
operation: "op",
|
|
326
|
+
point_index: "pt#",
|
|
327
|
+
rect_index: "rect#",
|
|
328
|
+
lon: "lon",
|
|
329
|
+
lat: "lat",
|
|
330
|
+
rect: "rect",
|
|
331
|
+
segments: "segments",
|
|
332
|
+
target_bytes: "target B",
|
|
333
|
+
payload_bytes: "payload B",
|
|
334
|
+
points_per_batch: "points/batch",
|
|
335
|
+
batches_per_sec: "batches/s",
|
|
336
|
+
ops_per_sec: "ops/s",
|
|
337
|
+
qps: "qps",
|
|
338
|
+
ns_per_op: "ns/op",
|
|
339
|
+
median_sec: "median s",
|
|
340
|
+
spread_pct: "spread %",
|
|
341
|
+
allocations_per_op: "alloc/op",
|
|
342
|
+
median_minor_gc: "minor GC",
|
|
343
|
+
median_major_gc: "major GC",
|
|
344
|
+
iterations: "iters",
|
|
345
|
+
operations: "ops",
|
|
346
|
+
threads: "threads",
|
|
347
|
+
entries: "entries",
|
|
348
|
+
rebuilds: "rebuilds",
|
|
349
|
+
queries: "queries",
|
|
350
|
+
cycle: "cycle",
|
|
351
|
+
rss_kb: "rss KB",
|
|
352
|
+
geom_memsize: "geom B",
|
|
353
|
+
flat_memsize: "flat B",
|
|
354
|
+
rtree_memsize: "rtree B",
|
|
355
|
+
rtree_over_flat: "rtree-flat B",
|
|
356
|
+
start_rss_kb: "start KB",
|
|
357
|
+
peak_rss_kb: "peak KB",
|
|
358
|
+
finish_rss_kb: "finish KB",
|
|
359
|
+
drift_kb: "drift KB",
|
|
360
|
+
max_drift_kb: "max drift KB",
|
|
361
|
+
elapsed_sec: "elapsed s",
|
|
362
|
+
sample_count: "samples"
|
|
363
|
+
}.freeze
|
|
364
|
+
|
|
365
|
+
def human_int(value)
|
|
366
|
+
Integer(value).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1_').reverse
|
|
367
|
+
rescue StandardError
|
|
368
|
+
value.to_s
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def human_rate(value)
|
|
372
|
+
v = Float(value)
|
|
373
|
+
return "0" if v.zero?
|
|
374
|
+
|
|
375
|
+
abs = v.abs
|
|
376
|
+
if abs >= 1_000_000
|
|
377
|
+
"%.2fM" % (v / 1_000_000.0)
|
|
378
|
+
elsif abs >= 1_000
|
|
379
|
+
"%.1fk" % (v / 1_000.0)
|
|
380
|
+
elsif abs >= 100
|
|
381
|
+
"%.1f" % v
|
|
382
|
+
else
|
|
383
|
+
"%.3f" % v
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def format_table_cell(key, value)
|
|
388
|
+
return "" if value.nil?
|
|
389
|
+
|
|
390
|
+
case key
|
|
391
|
+
when :ops_per_sec, :batches_per_sec, :qps
|
|
392
|
+
human_rate(value)
|
|
393
|
+
when :ns_per_op
|
|
394
|
+
"%.1f" % Float(value)
|
|
395
|
+
when :median_sec, :elapsed_sec
|
|
396
|
+
"%.4f" % Float(value)
|
|
397
|
+
when :spread_pct
|
|
398
|
+
"%.2f" % Float(value)
|
|
399
|
+
when :allocations_per_op
|
|
400
|
+
v = Float(value)
|
|
401
|
+
v < 0.01 ? ("%.5f" % v) : ("%.3f" % v)
|
|
402
|
+
when :lon, :lat
|
|
403
|
+
"%.3f" % Float(value)
|
|
404
|
+
when :rect
|
|
405
|
+
Array(value).map { |v| (Float(v) % 1).zero? ? Integer(v).to_s : ("%.3f" % Float(v)) }.join(":")
|
|
406
|
+
when :n, :iterations, :operations, :entries, :rebuilds, :queries, :cycle,
|
|
407
|
+
:rss_kb, :geom_memsize, :flat_memsize, :rtree_memsize, :rtree_over_flat,
|
|
408
|
+
:start_rss_kb, :peak_rss_kb, :finish_rss_kb, :drift_kb, :max_drift_kb,
|
|
409
|
+
:target_bytes, :payload_bytes, :segments, :points_per_batch, :threads,
|
|
410
|
+
:sample_count, :median_minor_gc, :median_major_gc
|
|
411
|
+
human_int(value)
|
|
412
|
+
when :full, :adaptive, :gc_disabled
|
|
413
|
+
value ? "yes" : "no"
|
|
414
|
+
else
|
|
415
|
+
value.to_s
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def table_columns(rows)
|
|
420
|
+
keys = rows.flat_map(&:keys).uniq
|
|
421
|
+
context = TABLE_CONTEXT_COLUMNS.select { |key| keys.include?(key) }
|
|
422
|
+
metrics = TABLE_METRIC_COLUMNS.select { |key| keys.include?(key) }
|
|
423
|
+
extras = keys - context - metrics - TABLE_INTERNAL_COLUMNS
|
|
424
|
+
context + extras + metrics
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def print_table(rows, columns)
|
|
428
|
+
headers = columns.map { |key| TABLE_HEADERS.fetch(key, key.to_s) }
|
|
429
|
+
body = rows.map do |row|
|
|
430
|
+
columns.map { |key| format_table_cell(key, row[key]) }
|
|
431
|
+
end
|
|
432
|
+
widths = headers.each_with_index.map do |header, index|
|
|
433
|
+
([header.length] + body.map { |cells| cells[index].length }).max
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
separator = "+-#{widths.map { |w| "-" * w }.join("-+-")}-+"
|
|
437
|
+
puts separator
|
|
438
|
+
puts "| #{headers.each_with_index.map { |h, i| h.ljust(widths[i]) }.join(" | ")} |"
|
|
439
|
+
puts separator
|
|
440
|
+
body.each do |cells|
|
|
441
|
+
puts "| #{cells.each_with_index.map { |cell, i| cell.rjust(widths[i]) }.join(" | ")} |"
|
|
442
|
+
end
|
|
443
|
+
puts separator
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def flush_reports
|
|
447
|
+
rows = report_rows
|
|
448
|
+
return if rows.empty?
|
|
449
|
+
|
|
450
|
+
rows.group_by { |row| row[:benchmark] }.each do |benchmark, group_rows|
|
|
451
|
+
notes, measured = group_rows.partition { |row| row.key?(:note) && row.keys.none? { |key| TABLE_METRIC_COLUMNS.include?(key) } }
|
|
452
|
+
|
|
453
|
+
notes.each do |row|
|
|
454
|
+
puts "\n-- #{benchmark} --"
|
|
455
|
+
puts row[:note]
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
next if measured.empty?
|
|
459
|
+
|
|
460
|
+
puts "\n-- #{benchmark} --"
|
|
461
|
+
print_table(measured, table_columns(measured))
|
|
462
|
+
end
|
|
463
|
+
ensure
|
|
464
|
+
rows&.clear
|
|
32
465
|
end
|
|
33
466
|
|
|
34
467
|
def box_wkt(min_x, min_y, max_x, max_y)
|
|
@@ -91,6 +524,11 @@ module TGGeometryBench
|
|
|
91
524
|
end
|
|
92
525
|
end
|
|
93
526
|
|
|
527
|
+
def repeated_points(kind, count)
|
|
528
|
+
seed = points_for(kind)
|
|
529
|
+
Array.new(count) { |i| seed[i % seed.length] }
|
|
530
|
+
end
|
|
531
|
+
|
|
94
532
|
def packed_points(points)
|
|
95
533
|
points.flatten.pack("d*")
|
|
96
534
|
end
|
|
@@ -110,6 +548,13 @@ module TGGeometryBench
|
|
|
110
548
|
|
|
111
549
|
def say_header(title)
|
|
112
550
|
puts "\n== #{title} =="
|
|
113
|
-
puts "ruby=#{RUBY_VERSION} platform=#{RUBY_PLATFORM}
|
|
551
|
+
puts "ruby=#{RUBY_VERSION} platform=#{RUBY_PLATFORM} sizes=#{sizes.join(":")} repeats=#{repeats} " \
|
|
552
|
+
"min_seconds=#{format_table_cell(:median_sec, min_seconds)} warmup=#{format_table_cell(:median_sec, warmup_seconds)} " \
|
|
553
|
+
"adaptive=#{adaptive? ? "yes" : "no"} gc_disabled=#{disable_gc_during_timed? ? "yes" : "no"}"
|
|
554
|
+
puts "output=table; set TGEOMETRY_BENCH_FORMAT=kv for machine-readable key=value lines" if table_output? && !kv_output?
|
|
114
555
|
end
|
|
115
556
|
end
|
|
557
|
+
|
|
558
|
+
at_exit do
|
|
559
|
+
TGGeometryBench.flush_reports if TGGeometryBench.table_output?
|
|
560
|
+
end
|
|
@@ -3,25 +3,57 @@
|
|
|
3
3
|
require_relative "_support"
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("batch_packed_vs_loop")
|
|
6
|
-
|
|
6
|
+
points_per_batch = TGGeometryBench.env_integer("TGEOMETRY_BENCH_BATCH_POINTS", 1_000, min: 1)
|
|
7
7
|
|
|
8
8
|
%i[compact long_thin overlapping].each do |kind|
|
|
9
9
|
TGGeometryBench.sizes.each do |size|
|
|
10
10
|
entries = TGGeometryBench.entries_for(kind, size)
|
|
11
|
-
points =
|
|
11
|
+
points = TGGeometryBench.repeated_points(kind, points_per_batch)
|
|
12
12
|
packed = TGGeometryBench.packed_points(points)
|
|
13
13
|
|
|
14
14
|
%i[flat rtree].each do |strategy|
|
|
15
15
|
index = TGGeometryBench.build_index(entries, strategy: strategy)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
scalar = TGGeometryBench.measure_counted(
|
|
18
|
+
initial_iterations: TGGeometryBench.initial_iterations(50),
|
|
19
|
+
operations_per_iteration: points.length
|
|
20
|
+
) do |iterations|
|
|
21
|
+
iterations.times do
|
|
22
|
+
points.each { |lon, lat| index.find_covering(lon, lat) }
|
|
23
|
+
end
|
|
19
24
|
end
|
|
20
|
-
|
|
25
|
+
|
|
26
|
+
batch = TGGeometryBench.measure_counted(
|
|
27
|
+
initial_iterations: TGGeometryBench.initial_iterations(50),
|
|
28
|
+
operations_per_iteration: points.length
|
|
29
|
+
) do |iterations|
|
|
21
30
|
iterations.times { index.covering_ids_batch_packed(packed) }
|
|
22
31
|
end
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
TGGeometryBench.report(
|
|
34
|
+
"batch_packed_vs_loop",
|
|
35
|
+
{
|
|
36
|
+
kind: kind,
|
|
37
|
+
n: size,
|
|
38
|
+
strategy: strategy,
|
|
39
|
+
mode: :scalar_loop,
|
|
40
|
+
points_per_batch: points.length,
|
|
41
|
+
batches_per_sec: scalar[:ops_per_sec] / points.length
|
|
42
|
+
},
|
|
43
|
+
stats: scalar
|
|
44
|
+
)
|
|
45
|
+
TGGeometryBench.report(
|
|
46
|
+
"batch_packed_vs_loop",
|
|
47
|
+
{
|
|
48
|
+
kind: kind,
|
|
49
|
+
n: size,
|
|
50
|
+
strategy: strategy,
|
|
51
|
+
mode: :packed_batch,
|
|
52
|
+
points_per_batch: points.length,
|
|
53
|
+
batches_per_sec: batch[:ops_per_sec] / points.length
|
|
54
|
+
},
|
|
55
|
+
stats: batch
|
|
56
|
+
)
|
|
25
57
|
end
|
|
26
58
|
end
|
|
27
59
|
end
|
data/benchmark/ewkb_roundtrip.rb
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "benchmark"
|
|
4
3
|
require_relative "_support"
|
|
5
4
|
|
|
5
|
+
TGGeometryBench.say_header("ewkb_roundtrip")
|
|
6
|
+
|
|
6
7
|
geom = TG::Geometry.polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], srid: 4326)
|
|
7
8
|
ewkb = geom.to_ewkb
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
100_000.times { TG::Geometry.parse_wkb(ewkb).to_ewkb }
|
|
12
|
-
end
|
|
10
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(5_000)) do |iterations|
|
|
11
|
+
iterations.times { TG::Geometry.parse_wkb(ewkb).to_ewkb }
|
|
13
12
|
end
|
|
14
13
|
|
|
14
|
+
TGGeometryBench.report(
|
|
15
|
+
"ewkb_roundtrip",
|
|
16
|
+
{ library: :tg_geometry, operation: :parse_wkb_to_ewkb, payload_bytes: ewkb.bytesize },
|
|
17
|
+
stats: stats
|
|
18
|
+
)
|
|
19
|
+
|
|
15
20
|
if ENV["WITH_RGEO"]
|
|
16
21
|
begin
|
|
17
22
|
require "rgeo"
|
|
18
23
|
factory = RGeo::Cartesian.factory(srid: 4326)
|
|
19
24
|
rgeo_geom = factory.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))")
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
100_000.times { factory.parse_wkt(rgeo_geom.as_text) }
|
|
24
|
-
end
|
|
26
|
+
rgeo_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(5_000)) do |iterations|
|
|
27
|
+
iterations.times { factory.parse_wkt(rgeo_geom.as_text) }
|
|
25
28
|
end
|
|
29
|
+
|
|
30
|
+
TGGeometryBench.report(
|
|
31
|
+
"ewkb_roundtrip",
|
|
32
|
+
{ library: :rgeo, operation: :wkt_parse, payload_bytes: rgeo_geom.as_text.bytesize },
|
|
33
|
+
stats: rgeo_stats
|
|
34
|
+
)
|
|
26
35
|
rescue LoadError
|
|
27
36
|
warn "rgeo is not installed"
|
|
28
37
|
end
|
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
require_relative "_support"
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("falcon_concurrency")
|
|
6
|
-
|
|
7
|
-
puts "Falcon/Async behavior remains an Pending decision until Roman approves a dedicated dependency/setup."
|
|
6
|
+
TGGeometryBench.report("falcon_concurrency_note", note: "thread_read_baseline_only_no_falcon_dependency")
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
entries_count = TGGeometryBench.env_integer("TGEOMETRY_BENCH_ENTRIES", 1_000, min: 1)
|
|
9
|
+
threads = TGGeometryBench.env_integer("TGEOMETRY_BENCH_THREADS", 4, min: 1)
|
|
10
|
+
entries = TGGeometryBench.compact_entries(entries_count)
|
|
10
11
|
index = TGGeometryBench.build_index(entries, strategy: :rtree)
|
|
11
|
-
|
|
12
|
-
iterations = TGGeometryBench.iterations(10_000)
|
|
12
|
+
points = TGGeometryBench.points_for(:compact)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
stats = TGGeometryBench.measure_counted(
|
|
15
|
+
initial_iterations: TGGeometryBench.initial_iterations(2_000),
|
|
16
|
+
operations_per_iteration: threads
|
|
17
|
+
) do |iterations_per_thread|
|
|
18
|
+
threads.times.map do |thread_index|
|
|
16
19
|
Thread.new do
|
|
17
|
-
|
|
18
|
-
lon, lat =
|
|
20
|
+
iterations_per_thread.times do |i|
|
|
21
|
+
lon, lat = points[(i + thread_index) % points.length]
|
|
19
22
|
index.find_covering(lon, lat)
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end.each(&:join)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
TGGeometryBench.report(
|
|
29
|
+
"falcon_concurrency",
|
|
30
|
+
{ threads: threads, entries: entries_count, strategy: :rtree, operation: :find_covering },
|
|
31
|
+
stats: stats
|
|
32
|
+
)
|
data/benchmark/feature_source.rb
CHANGED
|
@@ -40,7 +40,7 @@ end
|
|
|
40
40
|
|
|
41
41
|
TGGeometryBench.say_header("feature_source")
|
|
42
42
|
|
|
43
|
-
sizes =
|
|
43
|
+
sizes = TGGeometryBench.full? ? [100, 1_000, 10_000, 50_000] : [100, 1_000, 10_000]
|
|
44
44
|
|
|
45
45
|
sizes.each do |size|
|
|
46
46
|
json = FeatureSourceBenchData.feature_collection(size)
|
|
@@ -54,38 +54,64 @@ sizes.each do |size|
|
|
|
54
54
|
direct_index = nil
|
|
55
55
|
roundtrip_index = nil
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
ruby_entries = FeatureSourceBenchData.ruby_json_parse_entries(path)
|
|
57
|
+
ruby = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
58
|
+
iterations.times { ruby_entries = FeatureSourceBenchData.ruby_json_parse_entries(path) }
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
read_entries = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
62
|
+
iterations.times do
|
|
63
|
+
feature_entries = TG::Geometry::FeatureSource.read_entries_file(path, id: ["properties", "@id"])
|
|
64
|
+
end
|
|
63
65
|
end
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
read_features = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
68
|
+
iterations.times do
|
|
69
|
+
feature_rows = TG::Geometry::FeatureSource.read_features_file(path, id: ["properties", "@id"])
|
|
70
|
+
end
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
direct_index_stat = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
74
|
+
iterations.times do
|
|
75
|
+
direct_index = TG::Geometry::FeatureSource.build_index_file(path, id: ["properties", "@id"], strategy: :rtree)
|
|
76
|
+
end
|
|
71
77
|
end
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
# Ensure the roundtrip benchmark does not accidentally include read_entries.
|
|
80
|
+
feature_entries ||= TG::Geometry::FeatureSource.read_entries_file(path, id: ["properties", "@id"])
|
|
81
|
+
|
|
82
|
+
roundtrip_index_stat = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
83
|
+
iterations.times do
|
|
84
|
+
roundtrip_index = TG::Geometry::Index.build(feature_entries, via: :geojson, strategy: :rtree)
|
|
85
|
+
end
|
|
75
86
|
end
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
ruby_entries.length,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
read_plus_roundtrip_sec = read_entries[:median_sec] + roundtrip_index_stat[:median_sec]
|
|
89
|
+
read_plus_roundtrip_ops_per_sec = read_plus_roundtrip_sec.positive? ? read_entries[:iterations] / read_plus_roundtrip_sec : 0.0
|
|
90
|
+
|
|
91
|
+
common = {
|
|
92
|
+
n: size,
|
|
93
|
+
json_bytes: json.bytesize,
|
|
94
|
+
ruby_entries: ruby_entries.length,
|
|
95
|
+
feature_entries: feature_entries.length,
|
|
96
|
+
feature_rows: feature_rows.length,
|
|
97
|
+
direct_size: direct_index.size,
|
|
98
|
+
roundtrip_size: roundtrip_index.size,
|
|
99
|
+
rss_kb: TGGeometryBench.rss_kb
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :ruby_json_parse_entries), stats: ruby)
|
|
103
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :read_entries_file), stats: read_entries)
|
|
104
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :read_features_file), stats: read_features)
|
|
105
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :build_index_direct), stats: direct_index_stat)
|
|
106
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :build_index_from_prepared_entries), stats: roundtrip_index_stat)
|
|
107
|
+
TGGeometryBench.report(
|
|
108
|
+
"feature_source_end_to_end",
|
|
109
|
+
common.merge(
|
|
110
|
+
mode: :read_entries_plus_build_index,
|
|
111
|
+
median_sec: read_plus_roundtrip_sec,
|
|
112
|
+
ops_per_sec: read_plus_roundtrip_ops_per_sec
|
|
113
|
+
)
|
|
114
|
+
)
|
|
89
115
|
ensure
|
|
90
116
|
file.close!
|
|
91
117
|
end
|
data/benchmark/flat_vs_rtree.rb
CHANGED
|
@@ -3,25 +3,67 @@
|
|
|
3
3
|
require_relative "_support"
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("flat_vs_rtree")
|
|
6
|
-
iterations = TGGeometryBench.iterations(1_000)
|
|
7
6
|
|
|
8
7
|
%i[compact long_thin overlapping].each do |kind|
|
|
9
8
|
TGGeometryBench.sizes.each do |size|
|
|
10
9
|
entries = TGGeometryBench.entries_for(kind, size)
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
flat = nil
|
|
12
|
+
rtree = nil
|
|
14
13
|
|
|
15
|
-
TGGeometryBench.
|
|
16
|
-
|
|
17
|
-
rtree_time = Benchmark.realtime { iterations.times { @rtree.find_covering(lon, lat) } }
|
|
18
|
-
puts "kind=#{kind} n=#{size} query=point lon=#{lon} lat=#{lat} flat_sec=%.6f rtree_sec=%.6f flat_qps=%.2f rtree_qps=%.2f build_flat=%.6f build_rtree=%.6f" % [flat_time, rtree_time, iterations / flat_time, iterations / rtree_time, build_flat, build_rtree]
|
|
14
|
+
flat_build = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.25) do |iterations|
|
|
15
|
+
iterations.times { flat = TGGeometryBench.build_index(entries, strategy: :flat) }
|
|
19
16
|
end
|
|
17
|
+
rtree_build = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.25) do |iterations|
|
|
18
|
+
iterations.times { rtree = TGGeometryBench.build_index(entries, strategy: :rtree) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Rebuild once outside the build benchmark so query numbers do not depend on
|
|
22
|
+
# the last object produced by calibration/repeats.
|
|
23
|
+
flat = TGGeometryBench.build_index(entries, strategy: :flat)
|
|
24
|
+
rtree = TGGeometryBench.build_index(entries, strategy: :rtree)
|
|
25
|
+
|
|
26
|
+
TGGeometryBench.report("flat_vs_rtree_build", { kind: kind, n: size, strategy: :flat }, stats: flat_build)
|
|
27
|
+
TGGeometryBench.report("flat_vs_rtree_build", { kind: kind, n: size, strategy: :rtree }, stats: rtree_build)
|
|
28
|
+
|
|
29
|
+
TGGeometryBench.points_for(kind).each_with_index do |(lon, lat), point_index|
|
|
30
|
+
flat_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(2_000)) do |iterations|
|
|
31
|
+
iterations.times { flat.find_covering(lon, lat) }
|
|
32
|
+
end
|
|
33
|
+
rtree_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(2_000)) do |iterations|
|
|
34
|
+
iterations.times { rtree.find_covering(lon, lat) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
TGGeometryBench.report(
|
|
38
|
+
"flat_vs_rtree_point",
|
|
39
|
+
{ kind: kind, n: size, strategy: :flat, point_index: point_index, lon: lon, lat: lat },
|
|
40
|
+
stats: flat_stats
|
|
41
|
+
)
|
|
42
|
+
TGGeometryBench.report(
|
|
43
|
+
"flat_vs_rtree_point",
|
|
44
|
+
{ kind: kind, n: size, strategy: :rtree, point_index: point_index, lon: lon, lat: lat },
|
|
45
|
+
stats: rtree_stats
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
TGGeometryBench.rects_for(kind).each_with_index do |rect, rect_index|
|
|
50
|
+
flat_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(200)) do |iterations|
|
|
51
|
+
iterations.times { flat.intersecting_rect(*rect) }
|
|
52
|
+
end
|
|
53
|
+
rtree_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(200)) do |iterations|
|
|
54
|
+
iterations.times { rtree.intersecting_rect(*rect) }
|
|
55
|
+
end
|
|
20
56
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
TGGeometryBench.report(
|
|
58
|
+
"flat_vs_rtree_rect",
|
|
59
|
+
{ kind: kind, n: size, strategy: :flat, rect_index: rect_index, rect: rect },
|
|
60
|
+
stats: flat_stats
|
|
61
|
+
)
|
|
62
|
+
TGGeometryBench.report(
|
|
63
|
+
"flat_vs_rtree_rect",
|
|
64
|
+
{ kind: kind, n: size, strategy: :rtree, rect_index: rect_index, rect: rect },
|
|
65
|
+
stats: rtree_stats
|
|
66
|
+
)
|
|
25
67
|
end
|
|
26
68
|
end
|
|
27
69
|
end
|
data/benchmark/geom_query.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "benchmark"
|
|
4
3
|
require_relative "_support"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
STRATEGIES = %i[flat rtree].freeze
|
|
5
|
+
TGGeometryBench.say_header("geom_query")
|
|
8
6
|
|
|
7
|
+
COUNTS = (TGGeometryBench.full? ? [1_000, 10_000, 50_000] : [1_000, 10_000]).freeze
|
|
8
|
+
STRATEGIES = %i[flat rtree].freeze
|
|
9
9
|
|
|
10
10
|
def box(x, y, size = 1.0)
|
|
11
11
|
TG::Geometry.polygon([[x, y], [x + size, y], [x + size, y + size], [x, y + size], [x, y]])
|
|
@@ -18,16 +18,26 @@ COUNTS.each do |count|
|
|
|
18
18
|
[i, box(x, y)]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
queries = {
|
|
22
|
+
small: box(10.5, 10.5, 2.0),
|
|
23
|
+
large: box(0.5, 0.5, 120.0),
|
|
24
|
+
miss: box(-1_000.0, -1_000.0, 1.0)
|
|
25
|
+
}
|
|
23
26
|
|
|
24
|
-
puts "\n#{count} polygons"
|
|
25
27
|
STRATEGIES.each do |strategy|
|
|
26
28
|
index = TG::Geometry::Index.build(entries, via: :geom, strategy: strategy)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
queries.each do |query_name, query_geom|
|
|
31
|
+
initial = query_name == :large ? 100 : 1_000
|
|
32
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(initial)) do |iterations|
|
|
33
|
+
iterations.times { index.intersecting_geom_ids(query_geom) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
TGGeometryBench.report(
|
|
37
|
+
"geom_query",
|
|
38
|
+
{ n: count, strategy: strategy, query: query_name, method: :intersecting_geom_ids },
|
|
39
|
+
stats: stats
|
|
40
|
+
)
|
|
31
41
|
end
|
|
32
42
|
end
|
|
33
43
|
end
|
data/benchmark/gvl_threshold.rb
CHANGED
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
require_relative "_support"
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("gvl_threshold")
|
|
6
|
-
|
|
7
|
-
puts "This harness records baseline parse wall time only; it does not enable no-GVL execution."
|
|
6
|
+
TGGeometryBench.report("gvl_threshold_note", note: "baseline_only_gvl_is_held_no_public_release_gvl_knob")
|
|
8
7
|
|
|
9
|
-
# Build one valid WKT polygon close to the requested byte size.
|
|
10
|
-
# implementation accidentally benchmarked the same tiny 39-byte polygon for all
|
|
11
|
-
# target sizes because it used Array(...).first.
|
|
8
|
+
# Build one valid WKT polygon close to the requested byte size.
|
|
12
9
|
def polygon_wkt_at_least(target_bytes)
|
|
13
10
|
points_count = [4, target_bytes / 38].max
|
|
14
11
|
|
|
@@ -27,15 +24,17 @@ def polygon_wkt_at_least(target_bytes)
|
|
|
27
24
|
end
|
|
28
25
|
end
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
iterations = TGGeometryBench.iterations(2_000)
|
|
32
|
-
|
|
33
|
-
sizes.each do |target_bytes|
|
|
27
|
+
[128, 1_024, 16_384, 262_144].each do |target_bytes|
|
|
34
28
|
payload = polygon_wkt_at_least(target_bytes)
|
|
29
|
+
initial = target_bytes >= 262_144 ? 10 : 500
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(initial)) do |iterations|
|
|
37
32
|
iterations.times { TG::Geometry.parse_wkt(payload) }
|
|
38
33
|
end
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
TGGeometryBench.report(
|
|
36
|
+
"gvl_threshold_parse_wkt",
|
|
37
|
+
{ target_bytes: target_bytes, payload_bytes: payload.bytesize },
|
|
38
|
+
stats: stats
|
|
39
|
+
)
|
|
41
40
|
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "benchmark"
|
|
4
3
|
require_relative "_support"
|
|
5
4
|
|
|
5
|
+
TGGeometryBench.say_header("nearest_segment")
|
|
6
|
+
|
|
6
7
|
[100, 1_000, 10_000].each do |count|
|
|
7
8
|
points = count.times.map do |i|
|
|
8
9
|
angle = 2.0 * Math::PI * i / count
|
|
@@ -11,10 +12,13 @@ require_relative "_support"
|
|
|
11
12
|
points << points.first
|
|
12
13
|
ring = TG::Geometry.polygon(points).polygon.exterior_ring
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
x.report("1M nearest_segment calls") do
|
|
17
|
-
1_000_000.times { ring.nearest_segment(0.25, 0.33) }
|
|
18
|
-
end
|
|
15
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(10_000)) do |iterations|
|
|
16
|
+
iterations.times { ring.nearest_segment(0.25, 0.33) }
|
|
19
17
|
end
|
|
18
|
+
|
|
19
|
+
TGGeometryBench.report(
|
|
20
|
+
"nearest_segment",
|
|
21
|
+
{ segments: ring.num_segments, query: "0.25:0.33" },
|
|
22
|
+
stats: stats
|
|
23
|
+
)
|
|
20
24
|
end
|
|
@@ -8,10 +8,23 @@ TGGeometryBench.say_header("objectspace_memsize")
|
|
|
8
8
|
%i[compact long_thin overlapping].each do |kind|
|
|
9
9
|
TGGeometryBench.sizes.each do |size|
|
|
10
10
|
entries = TGGeometryBench.entries_for(kind, size)
|
|
11
|
+
TGGeometryBench.gc_start
|
|
11
12
|
flat = TGGeometryBench.build_index(entries, strategy: :flat)
|
|
13
|
+
TGGeometryBench.gc_start
|
|
12
14
|
rtree = TGGeometryBench.build_index(entries, strategy: :rtree)
|
|
13
15
|
geom = TG::Geometry.parse_geojson(entries.first.last)
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
TGGeometryBench.report(
|
|
18
|
+
"objectspace_memsize",
|
|
19
|
+
{
|
|
20
|
+
kind: kind,
|
|
21
|
+
n: size,
|
|
22
|
+
geom_memsize: ObjectSpace.memsize_of(geom),
|
|
23
|
+
flat_memsize: ObjectSpace.memsize_of(flat),
|
|
24
|
+
rtree_memsize: ObjectSpace.memsize_of(rtree),
|
|
25
|
+
rtree_over_flat: ObjectSpace.memsize_of(rtree) - ObjectSpace.memsize_of(flat),
|
|
26
|
+
rss_kb: TGGeometryBench.rss_kb
|
|
27
|
+
}
|
|
28
|
+
)
|
|
16
29
|
end
|
|
17
30
|
end
|
|
@@ -21,18 +21,21 @@ geojson = '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]
|
|
|
21
21
|
wkb = TG::Geometry.parse_wkt(small).to_wkb
|
|
22
22
|
|
|
23
23
|
cases = {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
24
|
+
"wkt_small" => [small, :wkt, 5_000],
|
|
25
|
+
"geojson_small" => [geojson, :geojson, 5_000],
|
|
26
|
+
"wkb_small" => [wkb, :wkb, 5_000],
|
|
27
|
+
"wkt_medium" => [medium, :wkt, 500],
|
|
28
|
+
"wkt_large" => [large, :wkt, 50]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
cases.each do |name, (payload, format)|
|
|
34
|
-
time = Benchmark.realtime do
|
|
31
|
+
cases.each do |name, (payload, format, initial)|
|
|
32
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(initial)) do |iterations|
|
|
35
33
|
iterations.times { TG::Geometry.parse(payload, format: format) }
|
|
36
34
|
end
|
|
37
|
-
|
|
35
|
+
|
|
36
|
+
TGGeometryBench.report(
|
|
37
|
+
"parse_throughput",
|
|
38
|
+
{ case: name, format: format, payload_bytes: payload.bytesize },
|
|
39
|
+
stats: stats
|
|
40
|
+
)
|
|
38
41
|
end
|
data/benchmark/rss_stability.rb
CHANGED
|
@@ -4,27 +4,28 @@ require_relative "_support"
|
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("rss_stability")
|
|
6
6
|
|
|
7
|
-
total_queries =
|
|
8
|
-
rebuilds =
|
|
9
|
-
entries_count =
|
|
10
|
-
max_drift_kb =
|
|
7
|
+
total_queries = TGGeometryBench.env_integer("TGEOMETRY_RSS_QUERIES", 10_000_000, min: 1)
|
|
8
|
+
rebuilds = TGGeometryBench.env_integer("TGEOMETRY_RSS_REBUILDS", 100, min: 1)
|
|
9
|
+
entries_count = TGGeometryBench.env_integer("TGEOMETRY_RSS_ENTRIES", 1_000, min: 1)
|
|
10
|
+
max_drift_kb = TGGeometryBench.env_integer("TGEOMETRY_RSS_MAX_DRIFT_KB", 51_200, min: 0)
|
|
11
11
|
|
|
12
12
|
queries_per_rebuild = (total_queries / rebuilds).clamp(1, total_queries)
|
|
13
13
|
entries = TGGeometryBench.compact_entries(entries_count)
|
|
14
14
|
points = TGGeometryBench.points_for(:compact)
|
|
15
15
|
packed_batch = TGGeometryBench.packed_points(points * 10)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
TGGeometryBench.gc_start
|
|
18
|
+
TGGeometryBench.gc_start
|
|
19
19
|
start_rss = TGGeometryBench.rss_kb
|
|
20
20
|
peak_rss = start_rss
|
|
21
21
|
samples = []
|
|
22
22
|
|
|
23
23
|
queries_executed = 0
|
|
24
|
-
started_at =
|
|
24
|
+
started_at = TGGeometryBench.monotonic
|
|
25
25
|
|
|
26
26
|
rebuilds.times do |cycle|
|
|
27
|
-
|
|
27
|
+
strategy = cycle.even? ? :flat : :rtree
|
|
28
|
+
index = TGGeometryBench.build_index(entries, strategy: strategy)
|
|
28
29
|
|
|
29
30
|
queries_per_rebuild.times do |q|
|
|
30
31
|
lon, lat = points[(q + cycle) % points.length]
|
|
@@ -39,29 +40,39 @@ rebuilds.times do |cycle|
|
|
|
39
40
|
|
|
40
41
|
index = nil
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
next unless (cycle % 10).zero?
|
|
44
|
+
|
|
45
|
+
TGGeometryBench.gc_start
|
|
46
|
+
rss = TGGeometryBench.rss_kb
|
|
47
|
+
peak_rss = [peak_rss, rss].max
|
|
48
|
+
samples << [cycle, queries_executed, rss]
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
TGGeometryBench.gc_start
|
|
52
|
+
TGGeometryBench.gc_start
|
|
52
53
|
finish_rss = TGGeometryBench.rss_kb
|
|
53
|
-
elapsed =
|
|
54
|
+
elapsed = TGGeometryBench.monotonic - started_at
|
|
54
55
|
drift_kb = finish_rss - start_rss
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
TGGeometryBench.report(
|
|
58
|
+
"rss_stability",
|
|
59
|
+
{
|
|
60
|
+
queries: queries_executed,
|
|
61
|
+
rebuilds: rebuilds,
|
|
62
|
+
entries: entries_count,
|
|
63
|
+
elapsed_sec: elapsed,
|
|
64
|
+
qps: queries_executed / elapsed,
|
|
65
|
+
start_rss_kb: start_rss,
|
|
66
|
+
peak_rss_kb: peak_rss,
|
|
67
|
+
finish_rss_kb: finish_rss,
|
|
68
|
+
drift_kb: drift_kb,
|
|
69
|
+
max_drift_kb: max_drift_kb,
|
|
70
|
+
sample_count: samples.length
|
|
71
|
+
}
|
|
60
72
|
)
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
samples.each { |row| puts " #{row.inspect}" }
|
|
74
|
+
samples.each do |cycle, queries, rss|
|
|
75
|
+
TGGeometryBench.report("rss_stability_sample", cycle: cycle, queries: queries, rss_kb: rss)
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
if max_drift_kb.positive? && drift_kb > max_drift_kb
|
|
@@ -3127,6 +3127,43 @@ static bool rtree_mark_candidate_iter(const double *min, const double *max, cons
|
|
|
3127
3127
|
return true;
|
|
3128
3128
|
}
|
|
3129
3129
|
|
|
3130
|
+
typedef struct {
|
|
3131
|
+
const tg_index_t *idx;
|
|
3132
|
+
const struct tg_geom *point;
|
|
3133
|
+
const tg_index_entry_t *best;
|
|
3134
|
+
long best_ordinal;
|
|
3135
|
+
} tg_rtree_find_args_t;
|
|
3136
|
+
|
|
3137
|
+
static bool rtree_find_covering_iter(const double *min, const double *max, const void *data,
|
|
3138
|
+
void *udata) {
|
|
3139
|
+
const tg_index_entry_t *entry = (const tg_index_entry_t *)data;
|
|
3140
|
+
tg_rtree_find_args_t *args = (tg_rtree_find_args_t *)udata;
|
|
3141
|
+
|
|
3142
|
+
(void)min;
|
|
3143
|
+
(void)max;
|
|
3144
|
+
|
|
3145
|
+
if (!entry) {
|
|
3146
|
+
return true;
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
if (entry->ordinal < 0 || entry->ordinal >= args->idx->len) {
|
|
3150
|
+
return true;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
if (args->best && entry->ordinal >= args->best_ordinal) {
|
|
3154
|
+
return true;
|
|
3155
|
+
}
|
|
3156
|
+
if (index_entry_matches_point(args->idx, entry, args->point)) {
|
|
3157
|
+
args->best = entry;
|
|
3158
|
+
args->best_ordinal = entry->ordinal;
|
|
3159
|
+
|
|
3160
|
+
if (entry->ordinal == 0) {
|
|
3161
|
+
return false;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
return true;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3130
3167
|
static unsigned char *rtree_candidate_marks(tg_index_t *idx, struct tg_rect query_rect) {
|
|
3131
3168
|
unsigned char *marks;
|
|
3132
3169
|
tg_rtree_mark_args_t args;
|
|
@@ -3195,33 +3232,31 @@ static VALUE index_find_covering_value(tg_index_t *idx, double lon, double lat)
|
|
|
3195
3232
|
|
|
3196
3233
|
if (idx->strategy == TG_GEOMETRY_INDEX_STRATEGY_RTREE) {
|
|
3197
3234
|
struct tg_rect point_rect = tg_rect_from_xyxy(lon, lat, lon, lat);
|
|
3198
|
-
|
|
3235
|
+
tg_rtree_find_args_t args;
|
|
3236
|
+
double min[2];
|
|
3237
|
+
double max[2];
|
|
3199
3238
|
|
|
3200
3239
|
point = tg_query_point_new(lon, lat);
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
}
|
|
3205
|
-
if (tg_geom_error(point)) {
|
|
3240
|
+
tg_query_point_raise_if_invalid(point);
|
|
3241
|
+
|
|
3242
|
+
if (!idx->rtree) {
|
|
3206
3243
|
tg_geom_free(point);
|
|
3207
|
-
|
|
3208
|
-
rb_raise(eTGGeometryError, "TG point geometry error");
|
|
3244
|
+
return Qnil;
|
|
3209
3245
|
}
|
|
3210
3246
|
|
|
3211
|
-
|
|
3212
|
-
|
|
3247
|
+
args.idx = idx;
|
|
3248
|
+
args.point = point;
|
|
3249
|
+
args.best = NULL;
|
|
3250
|
+
args.best_ordinal = 0;
|
|
3213
3251
|
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
break;
|
|
3220
|
-
}
|
|
3252
|
+
tg_rect_to_arrays(point_rect, min, max);
|
|
3253
|
+
rtree_search(idx->rtree, min, max, rtree_find_covering_iter, &args);
|
|
3254
|
+
|
|
3255
|
+
if (args.best) {
|
|
3256
|
+
result = args.best->id;
|
|
3221
3257
|
}
|
|
3222
3258
|
|
|
3223
3259
|
tg_geom_free(point);
|
|
3224
|
-
free(candidates);
|
|
3225
3260
|
return result;
|
|
3226
3261
|
}
|
|
3227
3262
|
|
data/lib/tg/geometry/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tg_geometry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Haydarov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|