lutaml-store 0.3.5 → 0.3.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6d1582c83685b6c2944eccf8c9c986aeba99e639e923dda11336551c696f459
4
- data.tar.gz: 91b2b36840f8303f1a70e094b4f7f50151665dcc21fa7a837e2b33c4e92400aa
3
+ metadata.gz: f523f159520c178e2855aa2070f1f7c19b39c4e6ddb21dd6efb9ee00dd786c55
4
+ data.tar.gz: 2184b3afe0a9fb1d697fd8fdf8f2f09f1d8664f30e389c3c41d7e33945f2f704
5
5
  SHA512:
6
- metadata.gz: d772bb3840f661fbad608973396ca00ae324950fa368ed9aaa14b53eab69e4b6618ec8cd57f9d2d29ffbc6f5ba93a138ca76fc176113d9a2a163ebf228df63cb
7
- data.tar.gz: b4822cac5f262a2a4235b7fd16455da525fa3c0405de17ed374cfd9327eeebc89cb22052243e0a04a1cd7fc00832f306b0c0bc9552c1faa967011f31a01066d7
6
+ metadata.gz: b63ebe9a382678154f315a5716c49ecfe7481ba3ec8d08b703cd89870e50cec980af56ade9cddb7f368e0e5e8f5798aa61b53061f9348c9a2d2335852d553859
7
+ data.tar.gz: ac4351a3a80e045d254a057026309b2e2cd4228598adfff7ff20ddd45b247b9ecbab82b92413a7acb99e14f20dcaa45b5ab30d5c40a2bb7b4ede64f0e4533262
data/Rakefile CHANGED
@@ -10,3 +10,48 @@ require "rubocop/rake_task"
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[spec rubocop]
13
+
14
+ desc "Run adapter benchmarks and print a comparison report"
15
+ task :"performance:compare" do
16
+ $LOAD_PATH.unshift(File.expand_path("lib", __dir__))
17
+ require "lutaml/store"
18
+ require "tmpdir"
19
+ require "fileutils"
20
+
21
+ ops = 2000
22
+ data = Array.new(100) { |i| { "id" => i, "name" => "item-#{i}" } }
23
+
24
+ measure = lambda do |label, &operation|
25
+ GC.start
26
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
+ operation.call
28
+ format("%<label>-34s %<us>9.1fus/op", label: label,
29
+ us: (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1_000_000 / ops)
30
+ end
31
+
32
+ results = []
33
+ Dir.mktmpdir do |dir|
34
+ memory = Lutaml::Store::Adapter::Memory.new
35
+ results << measure.call("memory set") { ops.times { |i| memory.set("k#{i}", data[i % 100]) } }
36
+ results << measure.call("memory get") { ops.times { |i| memory.get("k#{i % 500}") } }
37
+
38
+ fs = Lutaml::Store::Adapter::FileSystem.new(path: File.join(dir, "fs"))
39
+ results << measure.call("filesystem set") { ops.times { |i| fs.set("k#{i}", data[i % 100]) } }
40
+ results << measure.call("filesystem set (unchanged)") { ops.times { |i| fs.set("k#{i}", data[i % 100]) } }
41
+ results << measure.call("filesystem get") { ops.times { |i| fs.get("k#{i % 500}") } }
42
+ fs.close
43
+ FileUtils.rm_rf(File.join(dir, "fs"))
44
+
45
+ begin
46
+ require "sqlite3"
47
+ sqlite = Lutaml::Store::Adapter::Sqlite.new(path: File.join(dir, "db.sqlite3"))
48
+ results << measure.call("sqlite set") { ops.times { |i| sqlite.set("k#{i}", data[i % 100]) } }
49
+ results << measure.call("sqlite get") { ops.times { |i| sqlite.get("k#{i % 500}") } }
50
+ sqlite.close
51
+ rescue LoadError
52
+ results << "sqlite3 gem unavailable, sqlite benchmarks skipped"
53
+ end
54
+ end
55
+
56
+ puts results
57
+ end
@@ -53,6 +53,7 @@ module Lutaml
53
53
  @integrity_algorithm = @config.fetch(:integrity_algorithm, "sha256")
54
54
  @lock_path = File.expand_path(File.join(@root_path, LOCK_FILE))
55
55
  @monitor = self.class.monitor_for(@lock_path)
56
+ @metadata_cache = {}
56
57
 
57
58
  setup_directory_structure
58
59
  end
@@ -72,7 +73,16 @@ module Lutaml
72
73
 
73
74
  def set(key, value)
74
75
  content, format = encode_value(value)
75
- with_lock(:ex) { write_entry(key, content, format) }
76
+ with_lock(:ex) do
77
+ # Writing costs two fsync-backed temp files plus renames, and
78
+ # idempotent rewrites (periodic model saves of unchanged data)
79
+ # gain nothing from them, so an entry whose stored bytes and
80
+ # metadata are both unchanged is left untouched
81
+ file_path = existing_path_for_key(key)
82
+ next value if file_path && entry_unchanged?(file_path, content, format)
83
+
84
+ write_entry(key, content, format)
85
+ end
76
86
  value
77
87
  end
78
88
 
@@ -283,7 +293,7 @@ module Lutaml
283
293
 
284
294
  def read_verified_at(file_path, _key)
285
295
  data = file_safe_read(file_path)
286
- metadata = read_metadata(metadata_path_for(file_path))
296
+ metadata = cached_metadata(file_path)
287
297
 
288
298
  begin
289
299
  verify_data_integrity(data, metadata)
@@ -294,6 +304,11 @@ module Lutaml
294
304
  [:ok, decode_value(data, metadata[:format])]
295
305
  end
296
306
 
307
+ def entry_unchanged?(file_path, content, format)
308
+ File.binread(file_path) == content &&
309
+ read_metadata(metadata_path_for(file_path))[:format] == format
310
+ end
311
+
297
312
  def write_entry(key, content, format)
298
313
  file_path = path_for_key(key)
299
314
  metadata_path = metadata_path_for(file_path)
@@ -434,6 +449,30 @@ module Lutaml
434
449
  {}
435
450
  end
436
451
 
452
+ # Reading and parsing the metadata sidecar costs a read and a JSON
453
+ # parse on every get; the cached copy is validated against the
454
+ # file's mtime, so rewrites by this or another process invalidate
455
+ # it. Bounded like every cache fontist/lutaml hosts live with.
456
+ METADATA_CACHE_MAX = 1024
457
+
458
+ def cached_metadata(file_path)
459
+ metadata_path = metadata_path_for(file_path)
460
+ return {} unless File.exist?(metadata_path)
461
+
462
+ mtime = File.mtime(metadata_path)
463
+ entry = @metadata_cache[metadata_path]
464
+ if entry&.fetch(:mtime, nil) == mtime
465
+ @metadata_cache.delete(metadata_path)
466
+ @metadata_cache[metadata_path] = entry
467
+ return entry[:metadata]
468
+ end
469
+
470
+ metadata = read_metadata(metadata_path)
471
+ @metadata_cache.shift while @metadata_cache.size >= METADATA_CACHE_MAX
472
+ @metadata_cache[metadata_path] = { mtime: mtime, metadata: metadata }
473
+ metadata
474
+ end
475
+
437
476
  # ── Files ──
438
477
 
439
478
  def ensure_directory_exists(dir_path)
@@ -111,10 +111,11 @@ module Lutaml
111
111
  def bulk_set(key_value_pairs)
112
112
  @db.transaction do
113
113
  key_value_pairs.each do |key, value|
114
- serialized_value = value.is_a?(String) ? value : JSON.generate(value)
114
+ is_string = value.is_a?(String)
115
+ serialized_value = is_string ? value : JSON.generate(value)
115
116
  execute_statement(
116
- "INSERT OR REPLACE INTO #{@table_name} (key, value, updated_at) VALUES (?, ?, ?)",
117
- [key, serialized_value, Time.now.to_f]
117
+ "INSERT OR REPLACE INTO #{@table_name} (key, value, is_json, updated_at) VALUES (?, ?, ?, ?)",
118
+ [key, serialized_value, is_string ? 0 : 1, Time.now.to_f]
118
119
  )
119
120
  end
120
121
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Store
5
- VERSION = "0.3.5"
5
+ VERSION = "0.3.7"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ronald Tse