Skip to content

Commit 0ecc151

Browse files
authored
Fix lazy views (#7)
* use hash of IStructuralEquatable using the default comparer * use way simpler implementation * simplify view caching
1 parent 269f263 commit 0ecc151

File tree

3 files changed

+40
-75
lines changed

3 files changed

+40
-75
lines changed

src/Avalonia.FuncUI.UnitTests/LibTests.fs

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,20 @@
33
open Xunit
44
open System
55

6-
module LibTests =
7-
module FuncTests =
8-
open Avalonia.FuncUI.Lib
6+
module Library =
97

10-
type Msg = Increment | Decrement
8+
[<Fact>]
9+
let ``fun`` () =
10+
11+
let view () =
12+
let add = fun (count: int) -> count + 1
13+
let sub = fun (count: int) -> count - 1
14+
(add, sub)
1115

12-
type State = { count : int }
16+
let (add', sub') = view ()
17+
let (add'', sub'') = view ()
1318

14-
[<Fact>]
15-
let ``Comparing funcs`` () =
16-
17-
let a = fun (state: State, dispatch: Msg -> unit) -> dispatch Increment
18-
let b = fun (state: State, dispatch: Msg -> unit) -> dispatch Increment
19-
let c = fun (state: State, dispatch: Msg -> unit) -> dispatch Decrement
20-
let d = fun (state: State, dispatch: Msg -> unit) -> a(state, dispatch)
21-
22-
Assert.True(Func.compare a b)
23-
Assert.False(Func.compare a c)
24-
Assert.True(Func.isComparable a)
25-
Assert.True(Func.isComparable b)
26-
Assert.False(Func.isComparable d)
19+
Assert.Equal(add'.GetType(), add''.GetType())
20+
Assert.Equal(sub'.GetType(), sub''.GetType())
21+
Assert.NotEqual(add'.GetType(), sub'.GetType())
22+
Assert.NotEqual(add''.GetType(), sub''.GetType())

src/Avalonia.FuncUI/Core/Lib.fs

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,8 @@
11
namespace Avalonia.FuncUI.Lib
22

3-
[<RequireQualifiedAccess>]
4-
module Func =
5-
open System
6-
7-
(* get IL of method body *)
8-
let private getIL (func: 'a -> 'b) =
9-
let t = func.GetType()
10-
let m = t.GetMethod("Invoke")
11-
let b = m.GetMethodBody()
12-
b.GetILAsByteArray()
13-
14-
(* check if func is comparable *)
15-
let isComparable(func: 'a -> 'b) =
16-
let funcType = func.GetType()
17-
18-
let hasFields = funcType.GetFields()
19-
20-
if funcType.IsGenericType || not (Seq.isEmpty hasFields) then
21-
false
22-
else
23-
true
24-
25-
(* compares two functions for equality *)
26-
let compare (funcA: 'a -> 'b) (funcB: 'c -> 'd) : bool=
27-
if not (isComparable funcA) then
28-
raise (Exception("function 'funcA' is generic or has outer dependencies"))
29-
30-
if not (isComparable funcB) then
31-
raise (Exception("function 'funcB' is generic or has outer dependencies"))
32-
33-
let bytesA = getIL funcA
34-
let bytesB = getIL funcB
35-
let spanA = ReadOnlySpan(bytesA)
36-
let spanB = ReadOnlySpan(bytesB)
37-
spanA.SequenceEqual(spanB)
38-
39-
(* get hash of method body *)
40-
let hashMethodBody (func: 'a -> 'b) : int =
41-
let bytes = (getIL func) :> System.Collections.IStructuralEquatable
42-
bytes.GetHashCode()
3+
type MutableList<'t> = System.Collections.Generic.List<'t>
4+
type MutableDict<'key, 'value> = System.Collections.Generic.Dictionary<'key, 'value>
5+
type CuncurrentDict<'key, 'value> = System.Collections.Concurrent.ConcurrentDictionary<'key, 'value>
436

447
module Reflection =
458
open System.Reflection

src/Avalonia.FuncUI/DSL.base.fs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
namespace rec Avalonia.FuncUI
22

3-
open Avalonia.Controls
43
open System
54
open Types
65
open Avalonia.FuncUI.Lib
7-
open Types
86

97
type TypedAttr<'t> =
108
| Property of PropertyAttr
@@ -14,10 +12,11 @@ type TypedAttr<'t> =
1412
| Lifecycle of LifecylceAttr
1513

1614
[<AbstractClass; Sealed>]
17-
type Views private () =
15+
type Views () =
16+
17+
static let cache = CuncurrentDict<int, View>()
1818

19-
// TODO: Check if using a mutable hash map makes a big difference
20-
static let cache = ref Map.empty
19+
static member val CacheMaxLength : int = 1000 with get, set
2120

2221
(* create view - intended for internal use *)
2322
static member create<'t>(attrs: TypedAttr<'t> list) : View =
@@ -33,18 +32,25 @@ type Views private () =
3332
{ ViewType = typeof<'t>; Attrs = mappedAttrs; }
3433

3534
(* lazy views with caching *)
36-
static member viewLazy (state: 'state) (dispatch: 'dispatch) (func: 'state -> 'dispatch -> View) : View =
37-
let hash (state: 'state, func: 'state -> 'dispatch -> View) : int =
38-
Tuple(state, Func.hashMethodBody func).GetHashCode()
39-
40-
let key = hash(state, func)
41-
42-
match (!cache).TryFind key with
43-
| Some cached -> cached
44-
| None ->
45-
let computedValue = func state dispatch
46-
cache := (!cache).Add (key, computedValue)
47-
computedValue
35+
static member viewLazy (state: 'state, args: 'args, func: 'state -> 'args -> View) : View =
36+
37+
let key = Tuple(state, func.GetType()).GetHashCode()
38+
39+
if (cache.Count >= Views.CacheMaxLength) then
40+
cache.Clear()
41+
42+
printfn "cache length is %i" cache.Count
43+
44+
let hasValue, value = cache.TryGetValue key
45+
46+
match hasValue with
47+
| true -> value
48+
| false ->
49+
cache.AddOrUpdate(
50+
key,
51+
(fun _ -> func state args),
52+
(fun _ _ -> func state args)
53+
)
4854

4955

5056
[<AbstractClass; Sealed>]

0 commit comments

Comments
 (0)