commit c566e2cef17e758108427e8e01e1859888a1780d Author: Ivan Yuriev Date: Fri Dec 6 23:15:23 2024 +0300 initial commit featuring basic tree interaction and a benchmark diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6026a40 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "27.1.2" + gleam-version: "1.6.1" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7063e2 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# revault diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..8f26628 --- /dev/null +++ b/gleam.toml @@ -0,0 +1,21 @@ +name = "revault" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.45.0 and < 2.0.0" +gleam_otp = ">= 0.14.1 and < 1.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" +birl = ">= 1.7.1 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..f162689 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,17 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "gleam_erlang", version = "0.32.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "B18643083A0117AC5CFD0C1AEEBE5469071895ECFA426DCC26517A07F6AD9948" }, + { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, + { name = "gleam_stdlib", version = "0.45.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "206FCE1A76974AECFC55AEBCD0217D59EDE4E408C016E2CFCCC8FF51278F186E" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "ranger", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "B8F3AFF23A3A5B5D9526B8D18E7C43A7DFD3902B151B97EC65397FE29192B695" }, +] + +[requirements] +birl = { version = ">= 1.7.1 and < 2.0.0" } +gleam_otp = { version = ">= 0.14.1 and < 1.0.0" } +gleam_stdlib = { version = ">= 0.45.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/query.gleam b/src/query.gleam new file mode 100644 index 0000000..078928f --- /dev/null +++ b/src/query.gleam @@ -0,0 +1,38 @@ +import gleam/string +import tree + +pub type QueryError { + QueryError(String) +} + +pub fn get(tree, path, recursive) -> Result(tree.Tree, QueryError) { + let path = path |> parse + case path { + Error(err) -> Error(err) + Ok(path) -> { + let res = case recursive { + False -> tree.get(tree, path) + _ -> tree.rget(tree, path) + } + case res { + Error(_) -> Error(QueryError("There is no such node.")) + Ok(tree) -> Ok(tree) + } + } + } +} + +pub fn set(tree, path, data) -> Result(tree.Tree, QueryError) { + let path = path |> parse + case path { + Error(err) -> Error(err) + Ok(path) -> Ok(tree.set(tree, path, data)) + } +} + +pub fn parse(path) -> Result(List(String), QueryError) { + case path |> string.split(".") { + [head, ..tail] if head == "" -> Ok(tail) + _ -> Error(QueryError("Bad path.")) + } +} diff --git a/src/revault.gleam b/src/revault.gleam new file mode 100644 index 0000000..5086145 --- /dev/null +++ b/src/revault.gleam @@ -0,0 +1,6 @@ +import gleam/io +import gleam/string + +pub fn main() { + ".a.b.c" |> string.split(".") |> io.debug +} diff --git a/src/tree.gleam b/src/tree.gleam new file mode 100644 index 0000000..89556f2 --- /dev/null +++ b/src/tree.gleam @@ -0,0 +1,106 @@ +import gleam/dict + +pub type LeafData { + String(data: String) + Int(data: Int) + Float(data: Float) + Bool(data: Bool) + Null +} + +pub type Tree { + Root(children: dict.Dict(String, Tree)) + Node(children: dict.Dict(String, Tree)) + Leaf(data: LeafData) +} + +pub fn new() { + Root(dict.new()) +} + +/// Traverses the tree along the given path. +/// +/// If the last found node is a Leaf, returns it. +/// Else returns Nil +pub fn get(tree, path) { + let res = get_step(tree, path) + case res { + Error(_) -> Error(Nil) + Ok(_) as res -> res + } +} + +/// Recursive Get: returns whole requsted subtree +pub fn rget(tree, path) { + let res = get_step(tree, path) + case res { + Error(_) -> Error(Nil) + Ok(_) as res -> res + } +} + +fn get_step(tree: Tree, path: List(String)) -> Result(Tree, Nil) { + case tree, path { + Root(_) as rt, [] -> Ok(rt) + Root(children), [head] -> children |> dict.get(head) + Root(children), [head, ..tail] -> + case children |> dict.get(head) { + Error(_) as err -> err + Ok(child) -> get_step(child, tail) + } + Leaf(_) as lf, [] -> Ok(lf) + Leaf(_), [_, ..] -> Error(Nil) + Node(_) as nd, [] -> Ok(nd) + Node(children), [head, ..tail] -> + case children |> dict.get(head) { + Error(_) as err -> err + Ok(child) -> get_step(child, tail) + } + } +} + +pub fn set(tree, path, data) { + let res = set_step(tree, path, data) + res +} + +fn set_step(tree: Tree, path: List(String), data: LeafData) { + case tree, path { + Leaf(_), [child] -> Node(dict.new() |> dict.insert(child, Leaf(data))) + Node(children), [child] -> Node(children |> dict.insert(child, Leaf(data))) + Leaf(_), [head, ..tail] -> + Node( + dict.new() + |> dict.insert(head, set_step(dict.new() |> Node(), tail, data)), + ) + Node(children), [head, ..tail] -> + case children |> dict.get(head) { + Error(_) -> + Node( + children + |> dict.insert(head, set_step(dict.new() |> Node(), tail, data)), + ) + Ok(child) -> + Node(children |> dict.insert(head, set_step(child, tail, data))) + } + + Root(children), [head] -> + Root( + children + |> dict.insert(head, Leaf(data)), + ) + + Root(children), [head, ..tail] -> + case children |> dict.get(head) { + Error(_) -> + Root( + children + |> dict.insert(head, set_step(dict.new() |> Node(), tail, data)), + ) + Ok(child) -> + Root(children |> dict.insert(head, set_step(child, tail, data))) + } + + _, [] -> tree + } +} diff --git a/test/benchmark.gleam b/test/benchmark.gleam new file mode 100644 index 0000000..9b6fa4b --- /dev/null +++ b/test/benchmark.gleam @@ -0,0 +1,110 @@ +import birl +import birl/duration.{Duration} +import gleam/int +import gleam/io +import gleam/list +import gleam/otp/task +import gleam/string +import query +import tree + +pub fn main() { + kv_bench() + tree_bench() +} + +fn kv_bench() { + let ts = birl.now() + let tree = tree.new() |> fill(0) + let ts2 = birl.now() + let Duration(ms) = birl.difference(ts2, ts) + { "Set 1M key-value pairs in " <> string.inspect(ms / 10) <> " ms." } + |> io.debug + + let tasks = tree |> setup_tasks() + let ts = birl.now() + tasks |> list.map(fn(task) { task |> task.await_forever() }) + let ts2 = birl.now() + let Duration(ms) = birl.difference(ts2, ts) + { + "Read 1M key-value pairs using 10 actors in " + <> string.inspect(ms / 10) + <> " ms." + } + |> io.debug +} + +fn fill(tree, i) { + case i { + 1_000_000 -> tree + _ -> { + let assert Ok(tree) = + tree |> query.set("." <> int.to_string(i), tree.Int(i)) + tree |> fill(i + 1) + } + } +} + +fn get_100k(tree, i) { + case i { + 100_000 -> Nil + _ -> { + let rint = int.random(1_000_000) + let assert Ok(_) = + tree |> query.get("." <> rint |> int.to_string(), False) + get_100k(tree, i + 1) + } + } +} + +fn setup_tasks(tree) { + list.range(0, 10) + |> list.map(fn(_) { task.async(fn() { tree |> get_100k(0) }) }) +} + +fn tree_bench() { + let paths = list.range(0, 1_000_000) |> list.map(fn(n) { n |> path }) + let ts = birl.now() + let tree = tree.new() |> fill_tree(paths) + let ts2 = birl.now() + let Duration(ms) = birl.difference(ts2, ts) + { "Set 1M nodes in " <> string.inspect(ms / 10) <> " ms." } |> io.debug + + let tasks = tree |> setup_nodes_tasks(paths) + let ts = birl.now() + tasks |> list.map(fn(task) { task |> task.await_forever() }) + let ts2 = birl.now() + let Duration(ms) = birl.difference(ts2, ts) + { "Read 1M nodes using 10 actors in " <> string.inspect(ms / 10) <> " ms." } + |> io.debug +} + +fn fill_tree(tree, paths) { + paths + |> list.fold(tree, fn(tree, path) { + let assert Ok(tree) = tree |> query.set(path, tree.String("foo")) + tree + }) +} + +fn setup_nodes_tasks(tree, paths) { + let paths = paths |> list.sized_chunk(100_000) + list.range(0, 10) + |> list.map2(paths, fn(_, path) { + task.async(fn() { tree |> get_nodes(path) }) + }) +} + +fn get_nodes(tree, paths) { + paths + |> list.fold(tree, fn(tree, path) { + let _ = tree |> query.get(path, True) + tree + }) +} + +fn path(i: Int) { + { i |> int.to_string() |> string.to_graphemes() } + |> list.fold("", fn(path, s) { path <> s <> "." }) + |> string.reverse() +} diff --git a/test/revault_test.gleam b/test/revault_test.gleam new file mode 100644 index 0000000..867ddb2 --- /dev/null +++ b/test/revault_test.gleam @@ -0,0 +1,52 @@ +import gleam/dict +import gleeunit +import gleeunit/should +import query +import tree + +pub fn main() { + gleeunit.main() +} + +pub fn path_parse_test() { + ".a.b.c" |> query.parse() |> should.equal(Ok(["a", "b", "c"])) +} + +pub fn kv_test() { + let tree = tree.new() + let assert Ok(tree) = query.set(tree, ".a", tree.String("foo")) + let assert Ok(tree) = query.set(tree, ".b", tree.Int(42)) + let assert Ok(tree) = query.set(tree, ".c", tree.Float(12.34)) + let assert Ok(tree) = query.set(tree, ".d", tree.Bool(False)) + let assert Ok(tree) = query.set(tree, ".e", tree.Null) + + query.get(tree, ".a", False) + |> should.equal(tree.Leaf(tree.String("foo")) |> Ok()) + query.get(tree, ".b", False) + |> should.equal(tree.Leaf(tree.Int(42)) |> Ok()) + query.get(tree, ".c", False) + |> should.equal(tree.Leaf(tree.Float(12.34)) |> Ok()) + query.get(tree, ".d", False) + |> should.equal(tree.Leaf(tree.Bool(False)) |> Ok()) + query.get(tree, ".e", False) + |> should.equal(tree.Leaf(tree.Null) |> Ok()) +} + +pub fn tree_test() { + let tree = tree.new() + let assert Ok(tree) = query.set(tree, ".a.b.c.d", tree.String("foo")) + let assert Ok(tree) = query.set(tree, ".a.b2.c", tree.String("bar")) + let assert Ok(tree) = query.set(tree, ".a.b", tree.String("new foo")) + let assert Ok(tree) = query.set(tree, ".a2.b3", tree.Int(42)) + + query.get(tree, ".a.b", False) + |> should.equal(tree.Leaf(tree.String("new foo")) |> Ok()) + + query.get(tree, ".a.b2", True) + |> should.equal( + tree.Node(dict.new() |> dict.insert("c", tree.Leaf(tree.String("bar")))) + |> Ok(), + ) + query.get(tree, ".a2.b3", False) + |> should.equal(tree.Leaf(tree.Int(42)) |> Ok()) +}