Skip to content

Commit

Permalink
GC WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
point committed May 28, 2024
1 parent 6c7c74e commit ce5e181
Show file tree
Hide file tree
Showing 17 changed files with 643 additions and 55 deletions.
2 changes: 1 addition & 1 deletion lib/y/content/deleted.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ defmodule Y.Content.Deleted do
defstruct len: 0

def new(len), do: %Deleted{len: len}
def from_item(%Item{length: l}), do: %Deleted{len: l}
def from_item(%Item{} = item), do: %Deleted{len: Item.content_length(item)}
end
7 changes: 7 additions & 0 deletions lib/y/content/string.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Y.Content.String do
alias __MODULE__
defstruct [:str]

def new(str) when is_bitstring(str), do: %String{str: str}
def new(lst) when is_list(lst), do: %String{str: Enum.join(lst)}
end
1 change: 1 addition & 0 deletions lib/y/decoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ defmodule Y.Decoder do

# read deleted content
defp read_content(1, state, transaction) do
# {len, state} = State.read_and_advance(state, :rest, &read_uint/1)
{len, state} = State.read_len(state)
{[Y.Content.Deleted.new(len)], state, transaction}
end
Expand Down
3 changes: 3 additions & 0 deletions lib/y/doc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ defmodule Y.Doc do
alias Y.Transaction
alias Y.Decoder

require Logger

@type t :: %__MODULE__{
gc: term(),
gc_filter: (-> boolean()) | nil,
Expand Down Expand Up @@ -349,6 +351,7 @@ defmodule Y.Doc do
{:reply, {:ok, name}, new_transaction.doc}

error ->
Logger.warning("Transaction failed in doc #{inspect(doc.name)}")
{:reply, error, doc}
end
end
Expand Down
9 changes: 7 additions & 2 deletions lib/y/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Y.Encoder do
alias Y.Type
alias Y.Encoder.Buffer
alias Y.Content.Binary
alias Y.Content.Deleted

import Y.Encoder.Buffer, only: [write: 3]
import Y.Encoder.Operations
Expand Down Expand Up @@ -119,8 +120,8 @@ defmodule Y.Encoder do

defp write_parent_info(buf, _, _), do: buf

defp write_content(buf, %Item{content: content}, offset) do
len = length(content)
defp write_content(buf, %Item{content: content} = item, offset) do
len = Item.content_length(item)

buf
|> write(:length, len - offset)
Expand Down Expand Up @@ -161,6 +162,10 @@ defmodule Y.Encoder do
content = c.content
buf |> write(:rest, <<byte_size(content), content::binary>>)

match?(%Deleted{}, c) ->
# length already written
buf

is_struct(c) && Type.impl_for(c) != nil ->
buf |> write(:type_ref, Type.type_ref(c))

Expand Down
92 changes: 65 additions & 27 deletions lib/y/item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ defmodule Y.Item do
alias Y.Type
alias Y.Skip
alias Y.Content.Binary
alias Y.Content.JSON
alias Y.Content.Deleted
alias Y.Content.String, as: ContentString

require Logger

Expand Down Expand Up @@ -45,7 +47,12 @@ defmodule Y.Item do
valid_parent?(transaction, item)
end

def content_length(%Item{deleted?: true}), do: 0
def content_length(%Item{content: [%Y.Content.Binary{}]}), do: 1

def content_length(%Item{content: [%Y.Content.Deleted{len: len}]}), do: len
def content_length(%Item{content: [%Y.Content.Format{}]}), do: 1
def content_length(%Item{content: [%Y.Content.JSON{arr: arr}]}), do: length(arr)
def content_length(%Item{content: [%Y.Content.String{str: str}]}), do: length(str)
def content_length(%Item{content: content}) when is_list(content), do: length(content)
def content_length(%Item{}), do: 1
def content_length(%Skip{length: len}), do: len
Expand Down Expand Up @@ -77,30 +84,45 @@ defmodule Y.Item do
end
end

def split(%Item{content: content} = item, at_index)
when at_index >= 0 and at_index < length(content) do
{content_l, content_r} = Enum.split(content, at_index)
length_l = length(content_l)
length_r = length(content_r)
def split(%Item{content: content} = item, at_index) do
{content_l, content_r} =
case content do
[%Deleted{len: len}] when at_index >= 0 and at_index < len ->
{Deleted.new(at_index), Deleted.new(len - at_index)}

right_id = ID.new(item.id.client, item.id.clock + length_l)
[%JSON{arr: arr}] when at_index >= 0 and at_index < length(arr) ->
{l, r} = Enum.split(arr, at_index)
{JSON.new(l), JSON.new(r)}

item_l = %{
item
| length: length_l,
content: content_l,
origin: item.origin,
right_origin: nil
}
[%ContentString{str: str}] when at_index >= 0 ->
if at_index < String.length(str),
do: raise("String is too short to split at #{at_index}")

item_r = %{
item
| length: length_r,
content: content_r,
id: right_id,
origin: last_id(item_l),
right_origin: item.right_origin
}
{l, r} = String.split_at(str, at_index)
{ContentString.new(l), ContentString.new(r)}

content when is_list(content) ->
{_content_l, _content_r} = Enum.split(content, at_index)
end

item_l =
%{
item
| content: content_l,
origin: item.origin,
right_origin: nil
}
|> then(fn item -> %{item | length: content_length(item)} end)

item_r =
%{
item
| content: content_r,
origin: last_id(item_l),
right_origin: item.right_origin
}
|> then(fn item -> %{item | length: content_length(item)} end)
|> then(fn item -> %{item | id: ID.new(item.id.client, item.id.clock + item_l.length)} end)

{item_l, item_r}
end
Expand All @@ -112,12 +134,14 @@ defmodule Y.Item do
[f2 | _] = item2.content

if is_struct(f1) || is_struct(f2) do
is_struct(f1) && is_struct(f2) && f1.__struct__ == f2.__struct
is_struct(f1) && is_struct(f2) && f1.__struct__ == f2.__struct__
else
# && ID.equal?(item1.right_origin, item2.right_origin)
!is_struct(f1) && !is_struct(f2)
end
|> Kernel.&&(ID.equal?(item2.origin, Item.last_id(item1)))
|> Kernel.&&(
ID.equal?(item2.origin, Item.last_id(item1)) || ID.equal?(item2.origin, item1.origin)
)
|> Kernel.&&(ID.equal?(item1.right_origin, item2.right_origin))
|> Kernel.&&(item1.id.client == item2.id.client)
|> Kernel.&&(item1.id.clock + Item.content_length(item1) == item2.id.clock)
Expand All @@ -129,15 +153,29 @@ defmodule Y.Item do
if mergeable?(item1, item2) do
%Item{
item1
| content: item1.content ++ item2.content,
length: item1.length + item2.length,
| content: merge_content(item1.content, item2.content),
right_origin: item2.right_origin
}
|> then(fn item -> %{item | length: Item.content_length(item)} end)
else
raise "cannot merge unmerable items"
end
end

defp merge_content([%_{} = c1], [%_{} = c2]) do
case c1 do
%Y.Content.Binary{} -> [Y.Content.Binary.new(c1.content <> c2.content)]
%Y.Content.Deleted{} -> [Y.Content.Deleted.new(c1.len + c2.len)]
%Y.Content.JSON{} -> [Y.Content.JSON.new(c1.arr ++ c2.arr)]
%Y.Content.String{} -> [Y.Content.String.new(c1.str <> c2.str)]
_ -> [c1, c2]
end
end

defp merge_content(c1, c2) do
c1 ++ c2
end

def content_ref(%Item{content: [content | _]}) do
# () => { error.unexpectedCase() }, // GC is not ItemContent
# readContentDeleted, // 1
Expand Down Expand Up @@ -245,7 +283,7 @@ defmodule Y.Item do
end

def delete(%Item{deleted?: true} = item), do: item
def delete(%Item{} = item), do: %Item{item | deleted?: true}
def delete(%Item{} = item), do: %Item{item | deleted?: true, length: 0}

defp valid_origin?(_, %{origin: nil}), do: true

Expand Down
20 changes: 18 additions & 2 deletions lib/y/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,21 @@ defmodule Y.Transaction do
end
end)

share =
if doc.gc do
Enum.reduce(doc.share, %{}, fn {type_name, type}, share ->
case type_name in changed_names do
true -> Map.put(share, type_name, Type.gc(type))
false -> Map.put(share, type_name, type)
end
end)
else
doc.share
end

%{transaction | delete_set: delete_set}
|> merge_delete_sets_to_doc()
|> update_doc_share(share)
end

def force_pack(%Transaction{} = transaction), do: %{transaction | need_pack: true}
Expand Down Expand Up @@ -83,8 +96,8 @@ defmodule Y.Transaction do
Map.update(
ds,
item.id.client,
MapSet.new([{item.id.clock, item.length}]),
&MapSet.put(&1, {item.id.clock, item.length})
MapSet.new([{item.id.clock, Item.content_length(item)}]),
&MapSet.put(&1, {item.id.clock, Item.content_length(item)})
)

_ ->
Expand All @@ -108,6 +121,9 @@ defmodule Y.Transaction do
%{transaction | doc: %{doc | delete_set: ds}}
end

defp update_doc_share(%Transaction{doc: doc} = transaction, new_share),
do: %{transaction | doc: %{doc | share: new_share}}

# def cleanup(%Doc{} = doc, []), do: doc
#
# def cleanup(%Doc{} = doc, [transaction | rest_transactions_to_cleanup]) do
Expand Down
1 change: 1 addition & 0 deletions lib/y/type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defprotocol Y.Type do
def last(type, reference_item)
def delete(type, transaction, id)
def type_ref(type)
def gc(type)
end

# export const YArrayRefID = 0
Expand Down
30 changes: 30 additions & 0 deletions lib/y/type/array.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Y.Type.Array do
alias Y.Type.Array.ArrayTree
alias Y.Type.Unknown
alias Y.Type
alias Y.Content.Deleted
alias Y.Transaction
alias Y.Doc
alias Y.Item
Expand Down Expand Up @@ -253,6 +254,35 @@ defmodule Y.Type.Array do
defdelegate delete(array, transaction, id), to: Y.Type.Array, as: :delete_by_id

def type_ref(_), do: 0

def gc(%Array{tree: tree} = array) do
new_tree =
array
|> to_list(as_items: true, with_deleted: true)
|> Enum.filter(fn
%Item{content: [%Deleted{}]} -> false
%Item{deleted?: false} -> false
_ -> true
end)
|> case do
[] ->
tree

items ->
items
|> Enum.reduce(tree, fn deleted_item, tree ->
with %Item{} = item <- ArrayTree.find(tree, deleted_item.id),
{:ok, new_tree} <-
ArrayTree.replace(tree, item, [%{item | content: [Deleted.from_item(item)]}]) do
new_tree
else
_ -> tree
end
end)
end

%{array | tree: new_tree}
end
end

defimpl Enumerable do
Expand Down
2 changes: 1 addition & 1 deletion lib/y/type/array/array_tree.ex
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ defmodule Y.Type.Array.ArrayTree do
defp meter_object do
FingerTree.MeterObject.new(
fn %Item{id: id} = item ->
len = Item.content_length(item)
len = item.length

%Meter{
highest_clocks: %{id.client => id.clock},
Expand Down
21 changes: 21 additions & 0 deletions lib/y/type/map.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Y.Type.Map do
alias Y.Transaction
alias Y.Type
alias Y.Type.Unknown
alias Y.Content.Deleted
alias Y.Doc
alias Y.Item
alias Y.ID
Expand Down Expand Up @@ -416,5 +417,25 @@ defmodule Y.Type.Map do
defdelegate delete(map_type, transaction, key), to: Y.Type.Map, as: :delete

def type_ref(_), do: 1

def gc(%Y.Type.Map{map: map} = type_map) do
new_map =
map
|> Enum.reduce([], fn {k, v}, acc ->
case v do
[%Item{deleted?: false} | _] ->
[{k, v} | acc]

[%Item{content: [%Deleted{}]} | _] ->
[{k, v} | acc]

[%Item{} = item | rest] ->
[{k, [%{item | content: [Deleted.from_item(item)]} | rest]} | acc]
end
end)
|> Enum.into(%{})

%{type_map | map: new_map}
end
end
end
Loading

0 comments on commit ce5e181

Please sign in to comment.