Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/Specular/Internal/Incremental.purs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Specular.Internal.Incremental.Effect (foreachUntil)
import Specular.Internal.Incremental.Global (globalCurrentStabilizationNum, globalTotalRefcount, globalLastStabilizationNum, stabilizationIsNotInProgress)
import Specular.Internal.Incremental.Mutable (Field(..))
import Specular.Internal.Incremental.MutableArray as MutableArray
import Specular.Internal.Incremental.Node (Node, SomeNode, Observer, toSomeNode, toSomeNodeArray)
import Specular.Internal.Incremental.Node (Node, Observer, SomeNode, toSomeNode, toSomeNodeArray, traceEdgeAdd, traceEdgeRemove)
import Specular.Internal.Incremental.Node as Node
import Specular.Internal.Incremental.Optional (Optional)
import Specular.Internal.Incremental.Optional as Optional
Expand Down Expand Up @@ -100,13 +100,15 @@ addDependent = mkEffectFn2 \node dependent -> do
oldRefcount <- runEffectFn1 Node.refcount node
dependents <- runEffectFn1 Node.get_dependents node
runEffectFn2 MutableArray.push dependents dependent
runEffectFn2 traceEdgeAdd node dependent
runEffectFn2 handleRefcountChange node oldRefcount

removeDependent :: forall a. EffectFn2 (Node a) SomeNode Unit
removeDependent = mkEffectFn2 \node dependent -> do
oldRefcount <- runEffectFn1 Node.refcount node
dependents <- runEffectFn1 Node.get_dependents node
runEffectFn2 MutableArray.remove dependents dependent
runEffectFn2 traceEdgeRemove node dependent
runEffectFn2 handleRefcountChange node oldRefcount

handleRefcountChange :: forall a. EffectFn2 (Node a) Int Unit
Expand All @@ -132,13 +134,14 @@ handleRefcountChange = mkEffectFn2 \node oldRefcount -> do
-- - node has value computed
-- - node has correct height
connect :: forall a. EffectFn1 (Node a) Unit
connect = mkEffectFn1 \node -> do
connect = mkEffectFn1 \node -> runEffectFn2 Node.catchCycleError node do
mark <- runEffectFn1 Profiling.begin ("connect " <> Node.name node)

source <- runEffectFn1 Node.get_source node
dependencies <- source.dependencies

runEffectFn2 Array.iterate dependencies $ mkEffectFn1 \dependency -> do
-- runEffectFn2 dumpGraph ("before_connect_" <> Node.name node <> "_" <> Node.name dependency) node
runEffectFn2 addDependent dependency (toSomeNode node)
dependencyHeight <- runEffectFn1 Node.get_height dependency
adjustedHeight <- runEffectFn1 Node.get_adjustedHeight node
Expand All @@ -152,7 +155,14 @@ connect = mkEffectFn1 \node -> do
pure unit

value <- runEffectFn1 source.compute node
runEffectFn2 Node.set_value node value

-- Note: `compute` can return `none`, which means "do not update the value",
-- in cases where we're re-connecting a node which was previously alive.
-- In that case, don't erase the previous value.
if Optional.isSome value then
runEffectFn2 Node.set_value node value
else
pure unit

runEffectFn1 Profiling.end mark

Expand Down Expand Up @@ -185,7 +195,7 @@ stabilize = do
runEffectFn1 Profiling.end mark

recomputeNode :: EffectFn1 SomeNode Unit
recomputeNode = mkEffectFn1 \node -> do
recomputeNode = mkEffectFn1 \node -> runEffectFn2 Node.catchCycleError node do
height <- runEffectFn1 Node.get_height node
adjustedHeight <- runEffectFn1 Node.get_adjustedHeight node

Expand Down
171 changes: 169 additions & 2 deletions src/Specular/Internal/Incremental/Node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export function _new(none, source, dependents, observers, value, height) {
return {
const node = {
source: source,
dependents: dependents,
observers: observers,
Expand All @@ -14,6 +14,133 @@ export function _new(none, source, dependents, observers, value, height) {
// Hence -2.
changedAt: -2,
};
traceEvent({
type: "new",
node: getId(node),
dependencies: source.dependencies().map(getId),
});
return node;
}

class CycleError extends Error {
nodes;

constructor(node) {
super("X11 Possible cycle detected");
this.name = "CycleError";
this.nodes = [node];
console.error("Adding node to CycleError:", node.name);
}

toString() {
return (
super.toString() + ": " + this.nodes.map((node) => node.name).join(" -> ")
);
}
}

function findCycle(startNode) {
function go(node) {
if (node === startNode) {
throw new Error("cycle found");
}
for (const dep of node.dependents.array) {
console.log(`${node.name} <- ${dep.name}`);
go(dep);
}
}
for (const dep of startNode.dependents.array) {
console.log(`${startNode.name} <- ${dep.name}`);
go(dep);
}
}

const ids = new Map();
let dumpCounter = 0;

function getId(n) {
let id = ids.get(n);
if (!id) {
id = ids.size + 1;
ids.set(n, id);
}
return id;
}

function qname(n) {
return JSON.stringify(`${getId(n)}@${n.name.split(" ")[0]}`);
}

export const dumpGraph = (name, initialNode) => {
const visited = new Set();

let content = ["digraph {"];

function go(n) {
if (visited.has(n)) {
return;
}
visited.add(n);

for (const n2 of n.source.dependencies()) {
if (!n2) {
continue;
}
content.push(`${qname(n)} -> ${qname(n2)}`);
go(n2);
}
for (const n2 of n.dependents.array) {
if (!n2) {
continue;
}
go(n2);
}
}

getId(initialNode);

for (const n of ids.keys()) {
console.log(`dumping ${qname(n)}`);
go(n);
}

content.push("}");

global.debugFile(
`specular/${leftPadZeros(dumpCounter)}-${shortenName(name)}.gv`,
content.join("\n")
);
dumpCounter++;
};

function leftPadZeros(n) {
return n.toString().padStart(4, "0");
}

function shortenName(name) {
return name.substring(0, 100).replace(/ /g, "_").replace(/\.+$/, "");
}

export function _valueExc(Optional_none) {
return (node) => {
if (node.value === Optional_none) {
dumpGraph("valueExc", node);
throw new CycleError(node);
}
return node.value;
};
}

export function catchCycleError(node, block) {
try {
return block();
} catch (e) {
if (e instanceof CycleError) {
console.error("Adding node to CycleError:", node.name);
e.nodes.push(node);
}
throw e;
}
}

// [[[cog
Expand Down Expand Up @@ -58,6 +185,7 @@ export function get_adjustedHeight(node) {

export function set_adjustedHeight(node, value) {
node.adjustedHeight = value;
traceAttrChange(node, "adjustedHeight", value);
}

export function get_changedAt(node) {
Expand All @@ -74,6 +202,7 @@ export function get_height(node) {

export function set_height(node, value) {
node.height = value;
traceAttrChange(node, "height", value);
}

export function get_name(node) {
Expand All @@ -82,14 +211,52 @@ export function get_name(node) {

export function set_name(node, value) {
node.name = value;
traceAttrChange(node, "name", value);
}

export function get_value(node) {
return node.value;
}

export function set_value(node, value) {
export const set_value_impl = (Optional_none) => (node, value) => {
const prevHasValue = node.value !== Optional_none;
node.value = value;
const hasValue = node.value !== Optional_none;
if (prevHasValue !== hasValue) {
traceAttrChange(node, "hasValue", hasValue);
}
};

function traceAttrChange(node, attr, value) {
traceEvent({
type: "attrChange",
node: getId(node),
attr: attr,
value: value,
});
}

export function traceEdgeAdd(from, to) {
traceEvent({
type: "edgeAdd",
from: getId(from),
to: getId(to),
});
}

export function traceEdgeRemove(from, to) {
traceEvent({
type: "edgeRemove",
from: getId(from),
to: getId(to),
});
}

function traceEvent(event) {
if (global.debugFile) {
global.debugFile("specular/graph-log.txt", JSON.stringify(event) + "\n", {
append: true,
});
}
}
// [[[end]]]
22 changes: 16 additions & 6 deletions src/Specular/Internal/Incremental/Node.purs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ module Specular.Internal.Incremental.Node
import Prelude

import Effect (Effect)
import Specular.Internal.Incremental.Ref as Ref
import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn6, mkEffectFn1, mkEffectFn2, runEffectFn1, runEffectFn6)
import Effect.Unsafe (unsafePerformEffect)
import Specular.Internal.Incremental.Global (globalCurrentStabilizationNum)
import Specular.Internal.Incremental.Mutable (Any, Field(..), Immutable, Mutable)
import Specular.Internal.Incremental.MutableArray (MutableArray)
import Specular.Internal.Incremental.MutableArray as MutableArray
import Specular.Internal.Incremental.Mutable (Any, Field(..), Immutable, Mutable)
import Specular.Internal.Incremental.Optional (Optional)
import Specular.Internal.Incremental.Optional as Optional
import Specular.Internal.Incremental.Ref as Ref
import Specular.Internal.Profiling as Profiling
import Unsafe.Coerce (unsafeCoerce)

Expand Down Expand Up @@ -67,10 +67,16 @@ foreign import get_name :: forall a. EffectFn1 (Node a) (String)
foreign import set_name :: forall a. EffectFn2 (Node a) (String) Unit

foreign import get_value :: forall a. EffectFn1 (Node a) (Optional a)
foreign import set_value :: forall a. EffectFn2 (Node a) (Optional a) Unit
foreign import set_value_impl :: forall a. Optional a -> EffectFn2 (Node a) (Optional a) Unit

set_value :: forall a. EffectFn2 (Node a) (Optional a) Unit
set_value = set_value_impl Optional.none

-- [[[end]]]

foreign import traceEdgeAdd :: forall a b. EffectFn2 (Node a) (Node b) Unit
foreign import traceEdgeRemove :: forall a b. EffectFn2 (Node a) (Node b) Unit

foreign import _new
:: forall a
. EffectFn6
Expand Down Expand Up @@ -124,10 +130,12 @@ refcount = mkEffectFn1 \node -> do
numObservers <- runEffectFn1 MutableArray.length dependents
pure (numDependents + numObservers)

foreign import _valueExc :: forall a. Optional a -> EffectFn1 (Node a) a

valueExc :: forall a. EffectFn1 (Node a) a
valueExc = mkEffectFn1 \node -> do
value_opt <- runEffectFn1 get_value node
pure (Optional.fromSome value_opt)
valueExc = _valueExc Optional.none

foreign import catchCycleError :: forall a b. EffectFn2 (Node a) (Effect b) b

annotate :: forall a. EffectFn2 (Node a) String Unit
annotate = if Profiling.enabled then set_name else mkEffectFn2 \_ _ -> pure unit
Expand All @@ -140,3 +148,5 @@ isChangingInCurrentStabilization = mkEffectFn1 \node -> do
currentStabilizationNum <- runEffectFn1 Ref.read globalCurrentStabilizationNum
changedAt <- runEffectFn1 get_changedAt node
pure (changedAt == currentStabilizationNum)

foreign import dumpGraph :: forall a. EffectFn2 String (Node a) Unit
2 changes: 1 addition & 1 deletion src/Specular/Internal/Profiling.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ if (typeof global === "undefined") {
global = window;
}

// global.SPECULAR_PROFILING_ENABLED = true;
global.SPECULAR_PROFILING_ENABLED = true;

const enabled = !!global.SPECULAR_PROFILING_ENABLED;

Expand Down
16 changes: 16 additions & 0 deletions test/node/DynamicSpec.purs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,22 @@ spec = describe "Dynamic" $ do
-- clean up
liftEffect unsub1

it "works after disconnecting and reconnecting" $ withLeakCheck do
log <- liftEffect $ new []
root <- Ref.new 1
let dyn = uniqDynPure (Ref.value root)
unsub1 <- liftEffect $ execCleanupT do
subscribeEvent_ (append log) (changed dyn)
liftEffect unsub1

unsub2 <- liftEffect $ execCleanupT do
subscribeEvent_ (append log) (changed dyn)

readDynamic dyn `shouldReturn` 1

-- clean up
liftEffect unsub2

describe "foldDynMaybe" $ do
it "triggers only when function returns Just" $ withLeakCheck $ do
{ event, fire } <- liftEffect newEvent
Expand Down
2 changes: 2 additions & 0 deletions trace-viewer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
trace.jsonl
2 changes: 2 additions & 0 deletions trace-viewer/dist/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
10 changes: 10 additions & 0 deletions trace-viewer/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Specular Trace Viewer</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="app"></div>
<script src="dist/bundle.js"></script>
</body>
Loading
Loading