initial commit featuring basic tree interaction and a benchmark
This commit is contained in:
commit
c566e2cef1
23
.github/workflows/test.yml
vendored
Normal file
23
.github/workflows/test.yml
vendored
Normal file
@ -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
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*.beam
|
||||||
|
*.ez
|
||||||
|
/build
|
||||||
|
erl_crash.dump
|
||||||
21
gleam.toml
Normal file
21
gleam.toml
Normal file
@ -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"
|
||||||
17
manifest.toml
Normal file
17
manifest.toml
Normal file
@ -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" }
|
||||||
38
src/query.gleam
Normal file
38
src/query.gleam
Normal file
@ -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."))
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/revault.gleam
Normal file
6
src/revault.gleam
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import gleam/io
|
||||||
|
import gleam/string
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
".a.b.c" |> string.split(".") |> io.debug
|
||||||
|
}
|
||||||
106
src/tree.gleam
Normal file
106
src/tree.gleam
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
110
test/benchmark.gleam
Normal file
110
test/benchmark.gleam
Normal file
@ -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()
|
||||||
|
}
|
||||||
52
test/revault_test.gleam
Normal file
52
test/revault_test.gleam
Normal file
@ -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())
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user