Skip to content

Commit

Permalink
Fix lazy views (#7)
Browse files Browse the repository at this point in the history
* use hash of IStructuralEquatable using the default comparer

* use way simpler implementation

* simplify view caching
  • Loading branch information
JaggerJo authored Jun 30, 2019
1 parent 269f263 commit 0ecc151
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 75 deletions.
32 changes: 14 additions & 18 deletions src/Avalonia.FuncUI.UnitTests/LibTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,20 @@
open Xunit
open System

module LibTests =
module FuncTests =
open Avalonia.FuncUI.Lib
module Library =

type Msg = Increment | Decrement
[<Fact>]
let ``fun`` () =

let view () =
let add = fun (count: int) -> count + 1
let sub = fun (count: int) -> count - 1
(add, sub)

type State = { count : int }
let (add', sub') = view ()
let (add'', sub'') = view ()

[<Fact>]
let ``Comparing funcs`` () =

let a = fun (state: State, dispatch: Msg -> unit) -> dispatch Increment
let b = fun (state: State, dispatch: Msg -> unit) -> dispatch Increment
let c = fun (state: State, dispatch: Msg -> unit) -> dispatch Decrement
let d = fun (state: State, dispatch: Msg -> unit) -> a(state, dispatch)

Assert.True(Func.compare a b)
Assert.False(Func.compare a c)
Assert.True(Func.isComparable a)
Assert.True(Func.isComparable b)
Assert.False(Func.isComparable d)
Assert.Equal(add'.GetType(), add''.GetType())
Assert.Equal(sub'.GetType(), sub''.GetType())
Assert.NotEqual(add'.GetType(), sub'.GetType())
Assert.NotEqual(add''.GetType(), sub''.GetType())
43 changes: 3 additions & 40 deletions src/Avalonia.FuncUI/Core/Lib.fs
Original file line number Diff line number Diff line change
@@ -1,45 +1,8 @@
namespace Avalonia.FuncUI.Lib

[<RequireQualifiedAccess>]
module Func =
open System

(* get IL of method body *)
let private getIL (func: 'a -> 'b) =
let t = func.GetType()
let m = t.GetMethod("Invoke")
let b = m.GetMethodBody()
b.GetILAsByteArray()

(* check if func is comparable *)
let isComparable(func: 'a -> 'b) =
let funcType = func.GetType()

let hasFields = funcType.GetFields()

if funcType.IsGenericType || not (Seq.isEmpty hasFields) then
false
else
true

(* compares two functions for equality *)
let compare (funcA: 'a -> 'b) (funcB: 'c -> 'd) : bool=
if not (isComparable funcA) then
raise (Exception("function 'funcA' is generic or has outer dependencies"))

if not (isComparable funcB) then
raise (Exception("function 'funcB' is generic or has outer dependencies"))

let bytesA = getIL funcA
let bytesB = getIL funcB
let spanA = ReadOnlySpan(bytesA)
let spanB = ReadOnlySpan(bytesB)
spanA.SequenceEqual(spanB)

(* get hash of method body *)
let hashMethodBody (func: 'a -> 'b) : int =
let bytes = (getIL func) :> System.Collections.IStructuralEquatable
bytes.GetHashCode()
type MutableList<'t> = System.Collections.Generic.List<'t>
type MutableDict<'key, 'value> = System.Collections.Generic.Dictionary<'key, 'value>
type CuncurrentDict<'key, 'value> = System.Collections.Concurrent.ConcurrentDictionary<'key, 'value>

module Reflection =
open System.Reflection
Expand Down
40 changes: 23 additions & 17 deletions src/Avalonia.FuncUI/DSL.base.fs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
namespace rec Avalonia.FuncUI

open Avalonia.Controls
open System
open Types
open Avalonia.FuncUI.Lib
open Types

type TypedAttr<'t> =
| Property of PropertyAttr
Expand All @@ -14,10 +12,11 @@ type TypedAttr<'t> =
| Lifecycle of LifecylceAttr

[<AbstractClass; Sealed>]
type Views private () =
type Views () =

static let cache = CuncurrentDict<int, View>()

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

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

(* lazy views with caching *)
static member viewLazy (state: 'state) (dispatch: 'dispatch) (func: 'state -> 'dispatch -> View) : View =
let hash (state: 'state, func: 'state -> 'dispatch -> View) : int =
Tuple(state, Func.hashMethodBody func).GetHashCode()

let key = hash(state, func)

match (!cache).TryFind key with
| Some cached -> cached
| None ->
let computedValue = func state dispatch
cache := (!cache).Add (key, computedValue)
computedValue
static member viewLazy (state: 'state, args: 'args, func: 'state -> 'args -> View) : View =

let key = Tuple(state, func.GetType()).GetHashCode()

if (cache.Count >= Views.CacheMaxLength) then
cache.Clear()

printfn "cache length is %i" cache.Count

let hasValue, value = cache.TryGetValue key

match hasValue with
| true -> value
| false ->
cache.AddOrUpdate(
key,
(fun _ -> func state args),
(fun _ _ -> func state args)
)


[<AbstractClass; Sealed>]
Expand Down

0 comments on commit 0ecc151

Please sign in to comment.