From a77d72c37f55805b1122ccfee0c2046552081358 Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Wed, 17 Jun 2020 19:12:57 +0300 Subject: [PATCH 01/13] FEAT: scalable metered reactivity --- environment/reactivity.red | 654 ++++++++++++++++++++++++------------- modules/view/view.red | 26 +- 2 files changed, 448 insertions(+), 232 deletions(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index 9d67a6deea..106af3a325 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -3,170 +3,267 @@ Red [ Author: "Nenad Rakocevic" File: %reactivity.red Tabs: 4 - Rights: "Copyright (C) 2016-2018 Red Foundation. All rights reserved." + Rights: "Copyright (C) 2016-2020 Red Foundation. All rights reserved." License: { Distributed under the Boost Software License, Version 1.0. See https://github.com/red/red/blob/master/BSL-License.txt } ] -reactor!: context [ - on-change*: function [word old new][ - if system/reactivity/debug? [ - print [ - "-- on-change event --" lf - tab "word :" word lf - tab "old :" type? :old lf - tab "new :" type? :new - ] +system/reactivity: context [ + ;-- index format: [reaction [reactors..] ...] -- required by react/unlink 'all, dump-reactions, clear-reactions, stop-reactor + index: make hash! 1000 ;-- hash speeds up unlinking noticeably + + ;-- queue format: [reactor reaction target done] + queue: make hash! 100 + + debug?: no + metrics?: no + source: [] ;-- contains the initial [reactor reaction] that triggered a chain of subsequent reactions + + metrics: context [ + max-queue: max-index: max-reactors: max-relations: 0 + events: fired: queued: skipped: 0 + in-add: in-remove: in-react: in-check: in-eval: longest-flush: 0:0 + + reset: does [ + set self 0 + in-add: in-remove: in-react: in-check: in-eval: longest-flush: 0:0 ] - all [ - not empty? srs: system/reactivity/source - srs/1 = self - srs/2 = word - set-quiet in self word old ;-- force the old value - exit + + show: does [ + print "^/***** REACTIVITY METRICS REPORT *****" + print ["Metrics collection enabled?:" metrics?] + print "Statistical counts:" + print [" events triggered: " events] + print [" reactions fired: " fired "(immediately:" fired - queued ", queued:" queued ")" ] + print [" reactions skipped: " skipped] + print ["Time spent in reactions:" in-eval] + print "Time spent in reactivity:" + print [" total: " in-add + in-remove + in-react + in-check] + print [" adding relations: " in-add + in-react "(preparations:" in-react ")"] + print [" removing relations: " in-remove] + print [" dispatching: " in-check] + print [" longest queue flush:" longest-flush] + print "Peak values:" + print [" maximum queue size: " max-queue] + print [" maximum index size: " max-index] + print [" biggest relation: " max-reactors "reactors"] + print [" most used reactor: " max-relations "relations"] ] - unless all [block? :old block? :new same? head :old head :new][ - if any [series? :old object? :old][modify old 'owned none] - if any [series? :new object? :new][modify new 'owned reduce [self word]] + + start: target: none + time: func [/count 'into [word!] /save /local t] [ + t: now/precise + case [ + save [set target (get target) + t: difference t start t] + count [start: t target: into] + ] ] - system/reactivity/check/only self word + + ++: make op! func ['word value] [set word add get word value] + peak: func ['word value] [set word max get word value] + + register: func ['counter value] [set counter max get counter value] ] -] -deep-reactor!: make reactor! [ - on-deep-change*: function [owner word target action new index part][ - system/reactivity/check/only owner word + --measure--: func [code] [ + if metrics? [do bind code metrics] ] -] -reactor: function [spec [block!]][make reactor! spec] -deep-reactor: function [spec [block!]][make deep-reactor! spec] + --debug-print--: function [blk [block!] /full /no-gui] [ + all [ + debug? + any [not full debug? = 'full] + not all [no-gui attempt [system/console/gui?]] ;-- print in GUI console overflows stack in some places + ( + limit: (any [all [system/console system/console/size/x] 72]) - 10 + blk: reduce blk + forall blk [ + x: :blk/1 ;@@ workaround for #4517 + unless string? :x [ + if function? :x [x: body-of :x] + all [ + object? :x + pos: find/same values-of system/words x + x: pick words-of system/words index? pos + ] + change blk + replace/all + mold/flat/part :x limit + "make object!" "object" + ] + ] + if 1 < overshoot: (length? s: form blk) / limit [ ;-- trim longest parts equally + foreach x next blk [ ;-- don't trim the prefix + if 15 < n: length? x [ + clear skip x to integer! n / overshoot + ] + ] + clear skip s: form blk limit + ] + print s + ) + ] + ] + relations-of: func [reactor [object!]] [first body-of :reactor/on-change*] -system/reactivity: context [ - relations: make block! 1000 ;@@ change it to hash! once stable - ;-- queue format: [reactor reaction target done] - queue: make block! 100 - eat-events?: yes - debug?: no - source: [] + unique-objects: function [] [ ;-- used by debug funcs only + unique collect [foreach [_ list] index [keep list]] + ] + + relations-count: function [] [ + sum: 0 + foreach obj unique-objects [sum: (length? relations-of obj) / 3 + sum] + sum + ] - add-relation: func [ + add-relation: function [ obj [object!] - word + word [word!] reaction [block! function!] targets [set-word! block! object! none!] - /local new-rel ][ - new-rel: reduce [obj :word :reaction targets] - unless find/same/skip relations new-rel 4 [append relations new-rel] + --measure-- [time/count in-add] + new-rel: head reduce/into [:word :reaction targets] clear [] + relations: relations-of obj + unless find/same/skip relations new-rel 3 [ + append relations new-rel + unless objs: select/only/same/skip index :reaction 2 [ + reduce/into [:reaction objs: make block! 10] tail index + ] + unless find/same objs obj [append objs obj] + if block? targets [ ;-- react/link func .. [func objects..] case + foreach obj next targets [ + unless find/same objs obj [append objs obj] + ] + ] + --measure-- [ + peak max-relations (length? relations) / 3 + peak max-reactors length? objs + peak max-index (length? index) / 2 + ] + --debug-print-- ["-- react: added --" :reaction "FOR" word "IN" obj] + ] + --measure-- [time/save] ] - eval: function [code [block!] /safe][ + eval: function [code [block!] /safe /local result saved][ + --measure-- [ + time/count in-eval + fired ++ 1 + ] + --debug-print--/full ["-- react: firing --" either function? first code [body-of first code][code]] either safe [ - if error? set/any 'result try/all code [ - print :result + if error? error: try/all [set/any 'result do code 'ok] [ + print :error prin "*** Near: " print mold/part/flat code 80 - result: none ] - get/any 'result ][ - do code + set/any 'result do code ] + --measure-- [time/save] + :result ] - eval-reaction: func [reactor [object!] reaction [block! function!] target /mark][ - if mark [repend queue [reactor :reaction target yes]] - - either set-word? target [ - set/any target eval/safe :reaction - ][ - eval/safe any [all [block? :reaction reaction] target] + eval-reaction: func [reactor [object!] reaction [block! function!] target [set-word! block! object! none!]][ + case [ + set-word? target [set/any target eval/safe :reaction] + block? :reaction [eval/safe reaction] + 'linked [eval/safe target] ] ] pending?: function [reactor [object!] reaction [block! function!]][ - q: queue - while [q: find/same/skip q reactor 4][ - if same? :q/2 :reaction [return yes] - q: skip q 4 - ] - no + pattern: head reduce/into [reactor :reaction] clear [] + none <> find/same/skip queue pattern 4 ] - - check: function [reactor [object!] /only field [word! set-word!]][ - unless empty? pos: relations [ - while [pos: find/same/skip pos reactor 4][ - reaction: :pos/3 - if all [ - any [not only pos/2 = field] - any [empty? queue not pending? reactor :reaction] - ][ - either empty? queue [ - if empty? source [ - append source reactor - append source field - ] - eval-reaction/mark reactor :reaction pos/4 - - q: tail queue - until [ - q: skip q': q -4 - either q/4 [ ;-- was already executed? - clear q ;-- allow requeueing of it + + check: function [reactor [object!] field [word! set-word!] /local word reaction target][ + --debug-print--/full/no-gui ["-- react: checking --" field "IN" reactor] + --measure-- [time/count in-check] + pos: relations-of reactor + unless pos: find/skip pos field 3 [exit] + if initial?: tail? source [reduce/into [reactor field] source] + until [ + set [word reaction target] pos + case [ + pending? reactor :reaction [ ;-- don't allow cycles + --measure-- [skipped ++ 1] + --debug-print--/no-gui ["-- react: skipped --" :reaction "FOR" field "IN" reactor] + 'idle + ] + not tail? queue [ ;-- entered while another reaction is running + reduce/into [reactor :reaction target no] tail queue + --measure-- [ + queued ++ 1 + peak max-queue (length? queue) / 4 + ] + --debug-print--/no-gui ["-- react: queued --" :reaction "FOR" field "IN" reactor] + ] + 'else [ + reduce/into [reactor :reaction target yes] queue + --measure-- [peak max-queue 1 time/save] + eval-reaction reactor :reaction target + q: tail queue + while [not head? q] [ + q: skip q': q -4 + either q/4 [ ;-- was already executed? + clear q ;-- allow requeueing of it + ][ + eval-reaction q/1 :q/2 q/3 + either tail? q' [ ;-- queue wasn't extended + clear q ;-- allow requeueing ][ - eval-reaction q/1 :q/2 q/3 - either tail? q' [ ;-- queue wasn't extended - clear q ;-- allow requeueing - ][ - q/4: yes ;-- mark as executed - q: tail queue ;-- jump to recently queued reactions - ] + q/4: yes ;-- mark as executed + q: tail q ;-- jump to recently queued reactions ] - head? q - ] - clear queue - clear source - ][ - unless all [ - eat-events? - pending? reactor :reaction - ][ - repend queue [reactor :reaction pos/4 no] ] ] + --measure-- [time/count in-check] ] - pos: skip pos 4 ] + none? pos: find/skip skip pos 3 field 3 ] + if initial? [clear source] + --measure-- [peak longest-flush time/save] ] set 'stop-reactor function [ - face [object!] - /deep + "Forget all relations involving reactor OBJ" + obj [object!] "Face or reactor" + /deep "Deeply remove all relations from child faces" ][ - list: relations - while [not tail? list][ - either any [ - same? list/1 face - all [ - block? list/4 - pos: find/same list/4 face - empty? head remove pos - ] - ][ - remove/part list 4 - ][ - list: skip list 4 + --measure-- [time/count in-remove] + relations: relations-of obj + reactions: unique extract/into next relations 3 clear [] ;-- same reaction may be repeated many times for different words + foreach reaction reactions [ + --debug-print-- ["-- react: removed --" :reaction "FROM" obj] + pos: find/same/only/skip index :reaction 2 + remove find/same pos/2 obj + if tail? pos/2 [ + change pos end: skip tail pos -2 + clear end ] ] - if all [deep block? face/pane][foreach f face/pane [stop-reactor/deep f]] + clear relations + --measure-- [time/save] + + if all [deep block? :obj/pane] [ + foreach f obj/pane [stop-reactor/deep f] + ] + ] + + set 'clear-reactions function ["Remove all reactive relations"][ + foreach obj unique-objects [ + --debug-print-- ["-- react: clearing all relations -- FROM" obj] + relations: relations-of obj + clear relations + ] + clear index ] - - set 'clear-reactions function ["Removes all reactive relations"][clear relations] set 'dump-reactions function [ "Output all the current reactive relations for debugging purpose" @@ -174,52 +271,96 @@ system/reactivity: context [ limit: (any [all [system/console system/console/size/x] 72]) - 10 count: 0 - foreach [obj field reaction target] relations [ - prin count: count + 1 - prin ":---^/" - prin " Source: object " - list: words-of obj - remove find list 'on-change* - remove find list 'on-deep-change* - print mold/part list limit - 5 - prin " Field: " - print form field - prin " Action: " - print mold/flat/part :reaction limit - case [ - block? target [ - prin " Args: " - print copy/part replace/all mold/flat next target "make object!" "object" limit - ] - set-word? target [ - prin " Target: " - print form target + foreach reactor unique-objects [ + foreach [field reaction target] relations-of reactor [ + prin count: count + 1 + prin ":---^/" + prin " Source: object " + list: words-of reactor + remove find list 'on-change* + remove find list 'on-deep-change* + print mold/part list limit - 5 + prin " Field: " + print form field + prin " Action: " + print mold/flat/part :reaction limit + case [ + block? target [ + prin " Args: " + print copy/part replace/all mold/flat/part next target limit + 20 "make object!" "object" limit + ] + set-word? target [ + prin " Target: " + print form target + ] ] ] ] () ;-- avoids returning anything in the console ] - + + ;-- `is` should support non-object paths, like `pair/x`, `time/second`, `block/3` + ;; as well as in-path references: `anything/(pair/x)`, `any/:x/thing`... + ;; parsing summary: + ;; word: as first item in a path only (including inner paths) + ;; get-word: anywhere in a path, inside parens (including inner paths) e.g. `object/:x` + ;; get-words in lit-paths and set-paths? for now they are accepted; e.g. `obj/:x: y` + ;; lit-word: should be ignored? as it's a way to get around reactivity e.g. `set 'y get 'x` + ;; interop with react: react catches words after the object, not get-words; is - only first word in path is~: function [ "Defines a reactive relation whose result is assigned to a word" 'field [set-word!] "Set-word which will get set to the result of the reaction" reaction [block!] "Reactive relation" + /local item ][ + --measure-- [time/count in-react] obj: context? field - parse reaction rule: [ - any [ - item: word! (if in obj item/1 [add-relation obj item/1 reaction field]) - | any-path! | any-string! - | into rule - | skip + if reactor? obj [ ;-- skip global context (which would contain ALL words) and other normal objects -- let react handle them + words: clear [] + =add=: [(if in obj item [append words to word! item])] + =path=: [[set item word! =add= | skip] =path-rest=] + =set-path=: [skip =path-rest=] + =path-rest=: [ + any [ + set item get-word! =add= + | ahead paren! into =block= ;-- no literal paths, strings, blocks in paths (can't be constructed lexically) + | skip + ] + ] + parse reaction =block=: [ + any [ + set item [word! | get-word!] =add= + | any-string! + | ahead [path! | get-path! | lit-path!] into =path= + | ahead set-path! into =set-path= + | into =block= + | skip + ] ] + --measure-- [time/save] + foreach w words [add-relation obj w reaction field] ] react/later/with reaction field - set field either block? :reaction/1 [do :reaction/1][eval reaction] + set field either block? :reaction/1 [do reaction/1][eval reaction] ] set 'is make op! :is~ + for-all-paths: function ['word [word!] reaction [block!] code [block!]] [ + parse reaction rule: [ + any [ + item: [path! | lit-path! | get-path!] ( + set word item/1 + do code + parse item/1 rule ;-- process paren & inner paths + ) + | any-string! + | into rule ;-- also enters set-path + | skip + ] + ] + ] + set 'react? function [ "Returns a reactive relation if an object's field is a reactive source" reactor [object!] "Object to check" @@ -227,22 +368,60 @@ system/reactivity: context [ /target "Check if it's a target instead of a source" return: [block! function! word! none!] "Returns reaction, type or NONE" ][ + pos: relations-of reactor either target [ - pos: skip relations 3 - while [pos: find/skip pos field 4][ - if reactor = context? pos/1 [return pos/-1] - pos: skip pos 4 - ] + pos: at pos 3 + if pos: find/skip pos field 3 [reaction: :pos/-1] ;-- looks for a set-word ][ - pos: relations - while [pos: find/same/skip pos reactor 4][ - if pos/2 = field [return pos/3] - pos: skip pos 4 - ] + if pos: find/skip pos field 3 [reaction: :pos/2] + ] + all [ ;-- have to verify that on-change* block wasn't just copied into this object from other reactor + :reaction + objs: select/same/only/skip index :reaction 2 + find/same objs reactor + return :reaction ] none ] - + + get-object-path-length: function [path [any-path!] obj [word!]] [ + either 2 = length? path [ + set/any obj get/any path/1 + 1 + ][ + part: length? path + set obj none + until [ ;-- search for an object (deep first) + part: part - 1 + path: copy/part path part + any [ + object? set obj attempt [get path] + part = 1 + ] + ] + part + ] + ] + + unlink-reaction: function [reactor [object!] reaction [function! block!]] [ + pos: next relations-of reactor + while [pos: find/same/only/skip pos :reaction 3][ + --debug-print-- ["-- react: removed --" :reaction "FOR" pos/-1 "IN" reactor] + change back found?: pos end: skip tail pos -3 + clear end + ] + found? + ] + + set 'reactor? function ["Check if object is a reactor" obj [any-type!]] [ + all [ + object? :obj + oc: in obj 'on-change* + function? get/any oc ;-- can be unset when `is` is used in global context + any-block? relations-of obj ;-- is a reactor, not other object with on-change* + ] + ] + set 'react function [ "Defines a new reactive relation between two or more objects" reaction [block! function!] "Reactive relation" @@ -253,97 +432,130 @@ system/reactivity: context [ /later "Run the reaction on next change instead of now" /with "Specifies an optional face object (internal use)" ctx [object! set-word! none!] "Optional context for VID faces or target set-word" + /local item return: [block! function! none!] "The reactive relation or NONE if no relation was processed" ][ case [ link [ + --measure-- [time/count in-react] unless function? :reaction [cause-error 'script 'react-bad-func []] - objs: parse spec-of :reaction [ - collect some [keep word! | [refinement! | set-word!] break | skip] + args: clear [] + parse spec-of :reaction [ + collect into args some [keep word! | [refinement! | set-word!] break | skip] ] - if 2 > length? objs [cause-error 'script 'react-not-enough []] + if empty? args [cause-error 'script 'react-not-enough []] objects: reduce objects - if (length? objects) <> length? objs [cause-error 'script 'react-no-match []] + if (length? objects) <> length? args [cause-error 'script 'react-no-match []] unless parse objects [some object!][cause-error 'script 'react-bad-obj []] insert objects :reaction - - found?: no - parse body-of :reaction rule: [ - any [ - item: [path! | lit-path! | get-path!] ( - item: item/1 - if pos: find objs item/1 [ - obj: pick objects 1 + index? pos - add-relation obj item/2 :reaction objects - unless later [eval objects] - found?: yes - ] - ) - | set-path! | any-string! - | into rule - | skip + plan: clear [] + for-all-paths item body-of :reaction [ + all [ + word? :item/2 + pos: find args item/1 + obj: pick objects 1 + index? pos + reactor? :obj + found?: repend plan [obj item/2] ] ] + --measure-- [time/save] + foreach [obj word] plan [add-relation obj word :reaction objects] + if all [found? not later] [eval objects] ] unlink [ - if block? src [src: reduce src] - pos: relations - found?: no - while [pos: find/same/only pos :reaction][ - obj: pos/-2 - either any [src = 'all src = obj all [block? src find/same src obj]][ - pos: remove/part skip pos -2 4 - found?: yes - ][ - break + --measure-- [time/count in-remove] + case [ + object? src [ + if found?: unlink-reaction src :reaction [ + objs: select/only/same/skip index :reaction 2 + remove find/same objs src + ] + ] + block? src [ + objs: select/only/same/skip index :reaction 2 + foreach obj src [ + if unlink-reaction obj :reaction [ + remove find/same objs obj + found?: yes + ] + ] + ] + src = 'all [ + if pos: find/only/same/skip index :reaction 2 [ + foreach obj pos/2 [ + if unlink-reaction obj :reaction [found?: yes] + ] + clear pos/2 ;-- dissociate from all objects + change pos end: skip tail pos -2 + clear end + ] ] + 'else [cause-error 'script 'invalid-arg [src]] ] + --measure-- [time/save] ] 'else [ - found?: no - parse reaction rule: [ - any [ - item: [path! | lit-path! | get-path!] ( - saved: item/1 - if unset? attempt [get/any item: saved][ - cause-error 'script 'no-value [item] - ] - either 2 = length? item [ - set/any 'obj get/any item/1 - part: 1 - ][ - part: length? item - until [ ;-- search for an object (deep first) - part: part - 1 - path: copy/part item part - any [ - tail? path - object? obj: attempt [get path] - part = 1 - ] - ] - ] - if all [ - object? :obj ;-- rough checks for reactive object - in obj 'on-change* - ][ - part: part + 1 - add-relation obj item/:part reaction ctx - unless later [eval reaction] - found?: yes - ] - parse saved rule - ) - | set-path! | any-string! - | into rule - | skip + --measure-- [time/count in-react] + unless block? :reaction [cause-error 'script 'invalid-arg [:reaction]] + plan: clear [] + for-all-paths item reaction [ + if unset? attempt [get/any item][ + cause-error 'script 'no-value [item] + ] + part: get-object-path-length item 'obj + all [ + reactor? :obj + word? word: :item/(part + 1) + found?: repend plan [obj word] ] ] + --measure-- [time/save] + foreach [obj word] plan [add-relation obj word reaction ctx] + if all [found? not later] [eval reaction] ] ] either found? [:reaction][none] ;-- returns NONE if no relation was processed ] ] +reactor!: context [ + on-change*: function [word old [any-type!] new [any-type!]] [ + ;-- relations format: [reactor word reaction targets] + ;; src-word [reaction] set-word -- used by `is` (evaluates reaction, assigns to target) + ;; src-word function [func obj1 obj2...] -- used by react/link (evaluates target), one relation for every reactor in both list and func's body + ;; src-word [reaction] none -- used by react (evaluates reaction) + ;; src-word [reaction] set-word/object -- used by react/with (evaluates reaction, assigns to a set-word only) + [] ;-- relations placeholder (hash is ~10x times slower) + + sr: system/reactivity + sr/--debug-print--/full ["-- react: on-change --" word "FROM" type? :old "TO" type? :new] + sr/--measure-- [events ++ 1] + if all [ + not empty? srs: sr/source + srs/1 =? self + srs/2 = word + ][ + set-quiet in self word :old ;-- force the old value + sr/--measure-- [skipped ++ 1] + sr/--debug-print--/full ["-- react: protected --" word "VALUE" :old "IN" self] + exit + ] + unless all [series? :old series? :new same? head :old head :new][ + if any [series? :old object? :old][modify old 'owned none] + if any [series? :new object? :new][modify new 'owned reduce [self word]] + ] + sr/check self word + ] +] + +deep-reactor!: make reactor! [ + on-deep-change*: function [owner word target action new [any-type!] index part][ + system/reactivity/check owner word + ] +] + +reactor: function [spec [block!]][make reactor! spec] +deep-reactor: function [spec [block!]][make deep-reactor! spec] + diff --git a/modules/view/view.red b/modules/view/view.red index 50fa8811c3..f62347bbdd 100644 --- a/modules/view/view.red +++ b/modules/view/view.red @@ -291,7 +291,7 @@ on-face-deep-change*: function ["Internal use only" owner word target action new system/view/platform/on-change-facet owner word target action new index part ] ] - system/reactivity/check/only owner word + system/reactivity/check owner word ][ if any [ ;-- drop multiple changes on same facet none? state/3 @@ -351,7 +351,7 @@ update-font-faces: function ["Internal Use Only" parent [block! none!]][ if block? parent [ foreach f parent [ if f/state [ - system/reactivity/check/only f 'font + system/reactivity/check f 'font f/state/2: f/state/2 or 00080000h ;-- (1 << ((index? in f 'font) - 1)) if block? f/draw [ ;-- force a redraw in case the font in draw block f/state/2: f/state/2 or 00400000h ;-- (1 << ((index? in f 'draw) - 1)) @@ -388,6 +388,7 @@ face!: object [ ;-- keep in sync with facet! enum draw: none on-change*: function [word old new][ + [] if debug-info? self [ print [ "-- on-change event --" lf @@ -400,9 +401,11 @@ face!: object [ ;-- keep in sync with facet! enum if all [word <> 'state word <> 'extra][ all [ not empty? srs: system/reactivity/source - srs/1 = self + srs/1 =? self srs/2 = word - set-quiet in self word old ;-- force the old value + set-quiet in self word :old ;-- force the old value + system/reactivity/--measure-- [skipped ++ 1] + system/reactivity/--debug-print--/full ["-- react: protected --" word "VALUE" :old "IN" self] exit ] if word = 'pane [ @@ -455,7 +458,7 @@ face!: object [ ;-- keep in sync with facet! enum ] ] - system/reactivity/check/only self any [saved word] + system/reactivity/check self any [saved word] either state [ ;if word = 'type [cause-error 'script 'locked-word [type]] @@ -538,7 +541,7 @@ para!: object [ block? parent ][ foreach f parent [ - system/reactivity/check/only f 'para + system/reactivity/check f 'para system/view/platform/update-para f (index? in self word) - 1 ;-- sets f/state flag too if all [f/state f/state/1][show f] ] @@ -1173,18 +1176,18 @@ insert-event-func [ drop-list ['selected] ][none] - if facet [system/reactivity/check/only face facet] + if facet [system/reactivity/check face facet] ] if event/face/type = 'window [ switch event/type [ - move moving [system/reactivity/check/only event/face 'offset] - resize resizing [system/reactivity/check/only event/face 'size] + move moving [system/reactivity/check event/face 'offset] + resize resizing [system/reactivity/check event/face 'size] ] ] if event/type = 'select [ face: event/face if find [field area] face/type [ - system/reactivity/check/only face 'selected + system/reactivity/check face 'selected ] ] none @@ -1201,6 +1204,7 @@ insert-event-func [ all [not empty? face/text attempt/safer [load face/text]] all [face/options face/options/default] ] - system/reactivity/check/only face 'data + system/reactivity/check face 'data ] + none ] From 5a068cefb1765b4d10f12ce7857a13345559c2df Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Wed, 17 Jun 2020 19:13:16 +0300 Subject: [PATCH 02/13] TESTS: for #4510, #4507, #4471, #4176, #4166 --- tests/source/units/reactivity-test.red | 171 +++++++++++++++++++++---- 1 file changed, 144 insertions(+), 27 deletions(-) diff --git a/tests/source/units/reactivity-test.red b/tests/source/units/reactivity-test.red index 123bc583d5..991a867581 100644 --- a/tests/source/units/reactivity-test.red +++ b/tests/source/units/reactivity-test.red @@ -101,70 +101,112 @@ Red [ ===start-group=== "relations formation" - --test-- "rf-1" ; sanity check + --test-- "rf-1" ;-- sanity check rf-1-r: make reactor! [a: 1 b: is [a * 2]] - --assert 0 < length? system/reactivity/relations + --assert 0 < system/reactivity/relations-count clear-reactions - --assert empty? system/reactivity/relations + --assert 0 = system/reactivity/relations-count unset [rf-1-r] - --test-- "rf-2" ; shouldn't add duplicate relations + --test-- "rf-2" ;-- shouldn't add duplicate relations clear-reactions rf-2-r: make reactor! [a: 1 b: is [a * a * a]] - --assert 1 * 4 = length? system/reactivity/relations + --assert 1 = system/reactivity/relations-count unset [rf-2-r] - --test-- "rf-3" ; same + --test-- "rf-3" ;-- same clear-reactions - do [ ; FIXME: workaround for #3797 + do [ ;@@ FIXME: workaround for #3797 rf-3-r: make reactor! [a: b: 1 react [self/b: self/a * self/a * self/a]] - --assert 1 * 4 = length? system/reactivity/relations + --assert 1 = system/reactivity/relations-count --assert (rf-3-r/a: 2 rf-3-r/b = 8) ] unset [rf-3-r] - --test-- "rf-4" ; same + --test-- "rf-4" ;-- same clear-reactions - do [ ; FIXME: workaround for #3797 + do [ ;@@ FIXME: workaround for #3797 rf-4-r: make reactor! [a: b: c: 1 react [self/c: self/a * self/a * self/b * self/b]] - --assert 2 * 4 = length? system/reactivity/relations + --assert 2 = system/reactivity/relations-count --assert (rf-4-r/a: 2 rf-4-r/c = 4) --assert (rf-4-r/b: 2 rf-4-r/c = 16) ] unset [rf-4-r] - --test-- "rf-5" ; same + --test-- "rf-5" ;-- same clear-reactions - do [ ; FIXME: workaround for #3797 + do [ ;@@ FIXME: workaround for #3797 rf-5-r: make reactor! [ a: b: c: d: 1 - b: is [a + a] ; +1 - react [self/c: self/a * self/b * a * b] ; +2 - react [self/d: self/a + self/b + self/c + a + b + c] ; +3 + b: is [a + a] ;-- +1 + react [self/c: self/a * self/b * a * b] ;-- +2 + react [self/d: self/a + self/b + self/c + a + b + c] ;-- +3 ] - --assert 6 * 4 = length? system/reactivity/relations + --assert 6 = system/reactivity/relations-count --assert (rf-5-r/a: 2 rf-5-r/b = 4) --assert rf-5-r/c = 64 --assert rf-5-r/d = (2 + 4 + 64 * 2) ] unset [rf-5-r] - --test-- "rf-6" ; #3333 where `is` produced an excessive reaction with the wrong target object + --test-- "rf-6" ;-- #3333 where `is` produced an excessive reaction with the wrong target object clear-reactions rf-6r: make reactor! [x: 1] rf-6c: context [ x: is [rf-6r/x] ] - --assert 1 * 4 = length? system/reactivity/relations ;-- should only be a single reaction - --assert rf-6r = :system/reactivity/relations/1 ;-- `r` should be the source object + --assert 1 = system/reactivity/relations-count ;-- should only be a single reaction + --assert rf-6r =? :system/reactivity/index/2/1 ;-- `rf-6r` should be the source object unset [rf-6c rf-6r] - --test-- "rf-7" ; #3333 triple-reaction case + --test-- "rf-7" ;-- #3333 triple-reaction case clear-reactions rf-7r: make reactor! [x: 1] rf-7x: is [rf-7r/x] - --assert 1 * 4 = length? system/reactivity/relations ;-- should only be a single reaction - --assert rf-7r = :system/reactivity/relations/1 ;-- `r` should be the source object + --assert 1 = system/reactivity/relations-count ;-- should only be a single reaction + --assert rf-7r = :system/reactivity/index/2/1 ;-- `rf-7r` should be the source object unset [rf-7r rf-7x] + --test-- "rf-8" ;-- correct path recognition + clear-reactions + rf-8r: make deep-reactor! [p: 1x1] ;-- deep-reactor to watch for p/x change + rf-8x: is [rf-8r/p/x] ;-- single reaction on `rf-8r/p` + --assert 1 = system/reactivity/relations-count + --assert (rf-8r/p: 2x2 rf-8x = 2) + --assert (rf-8r/p/x: 3 rf-8x = 3) + --assert rf-8r/p = 3x2 + unset [rf-8r rf-8x] + + --test-- "rf-9" ;-- correct non-object path recognition + clear-reactions + rf-9r: make deep-reactor! [p: 1x1 x: is [p/x]] ;-- `is` must register p in p/x + --assert 1 = system/reactivity/relations-count + --assert rf-9r/x = 1 + --assert (rf-9r/p: 2x2 rf-9r/x = 2) + --assert (rf-9r/p/x: 3 rf-9r/x = 3) + --assert rf-9r/p = 3x2 + unset [rf-9r] + + --test-- "rf-10" ;-- correct non-object path recognition + clear-reactions + do [ ;@@ FIXME: workaround for #3797 + rf-10r: make reactor! [a: 1 b: 2 c: 3 w: 'a x: is [self/:w]] ;-- `is` must register :w in p/x + --assert 1 = system/reactivity/relations-count + --assert rf-10r/x = 1 + --assert (rf-10r/w: 'b rf-10r/x = 2) + --assert (rf-10r/w: 'c rf-10r/x = 3) + ] + unset [rf-10r] + + --test-- "rf-11" ;-- correct non-object path recognition; nested + clear-reactions + rf-11r: make deep-reactor! [ + p: 1x1 b: [a [1 2] b [3 4]] w: 'a + x: is [b/:w/(p/x)] ;-- `is` must register b, :w, and p in p/x + ] + --assert 3 = system/reactivity/relations-count + --assert rf-11r/x = 1 + --assert (rf-11r/w: 'b rf-11r/x = 3) + --assert (rf-11r/p/x: 2 rf-11r/x = 4) + unset [rf-11r] ;-- final group cleanup clear-reactions @@ -179,11 +221,86 @@ Red [ --assert a3091/c = 2 --test-- "#4022" - do [ ;-- force through interpreter as - a4022: make reactor! [i: repeat i 2 [i]] ;-- `repeat` returns unset when compiled. - --assert a4022/i = 2 + a4022: make reactor! [i: repeat i 2 [i]] + --assert a4022/i = 2 + + --test-- "#4166-1" + r4166: make reactor! [x: y: 1] + react b4166: [r4166/y: 2 * r4166/x] + --assert b4166 =? react? r4166 'x + clear-reactions + + --test-- "#4166-2" + do [ ;@@ FIXME: compiler can't swallow such `on-change*` + r4166: make reactor! [x: 1 y: is [x * 2]] + o4166: object [ + on-change*: func spec-of :r4166/on-change* copy/deep body-of :r4166/on-change* + x: 1 y: 2 + ] + --assert block? react?/target r4166 'y + --assert not react?/target o4166 'y ;-- should not be mistaken for r4166 ] - + clear-reactions + + --test-- "#4166-3" + do [ ;@@ FIXME: compiler can't swallow such `on-change*` + r4166: make reactor! spec: [x: 1 y: is [x * 2]] + o4166: object [ + on-change*: func spec-of :r4166/on-change* copy/deep body-of :r4166/on-change* + x: 1 y: 2 + ] + react/unlink last spec o4166 + --assert (r4166/x: 3 r4166/y = 6) ;-- should not be unlinked + ] + clear-reactions + + --test-- "#4176" + r4176: make reactor! [n: 1 c: is [1x1 * n] x: is [c/x] t: is [n * 1:0:0] hms: is [rejoin [t/hour t/minute t/second]]] + r4176/n: 2 + --assert r4176/c = 2x2 + --assert r4176/x = 2 + --assert r4176/t = 2:0:0 + --assert none <> find/match r4176/hms "200" ;-- allow both "200" and "200.0" + clear-reactions + + --test-- "#4471-1" + r4471: make reactor! [x: 1] + b4471: [] + react [all [:r4471/x integer? r4471/x append b4471 r4471/x]] + r4471/x: 2 + r4471/x: 3 + --assert b4471 = [1 2 3] + clear-reactions + + --test-- "#4471-2" + a4471: make deep-reactor! [text: 0] + b4471: make deep-reactor! [text: 0] + n4471: 0 + react/link func [a b] [n4471: n4471 + 1 a/text b/text b/text b/text] [a4471 b4471] + --assert 1 = n4471 + --assert (b4471/text: 1 2 = n4471) + clear-reactions + + ;@@ FIXME: this requires `print` mockup, else it outputs the dump + ;@@ FIXME: this requires images to be garbage-collected, else they stay in RAM + ; --test-- "#4507" + ; a4507: make deep-reactor! [i: make image! 2000x2000] + ; b4507: make deep-reactor! [i: make image! 2000x2000 a: a4507] + ; react/link func [a b] [b/i] [a4507 b4507] + ; t04507: now/precise + ; dump-reactions + ; dt4507: difference now/precise t04507 + ; --assert dt4507 < 0:0:1 ;-- took ~10 sec originally + ; clear-reactions + + --test-- "#4510" + a4510: make deep-reactor! [data: 0] + b4510: make deep-reactor! [data: 0] + react/later [a4510/('data)] + react/link/later func [a b] [a/('data)] [a4510 b4510] + --assert 0 = system/reactivity/relations-count + clear-reactions + ===end-group=== From 7ff112193f44232b1d9b2771fc942f8c60f24673 Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Wed, 17 Jun 2020 21:20:30 +0300 Subject: [PATCH 03/13] FIX: workarounds for #4526, #4527 --- environment/reactivity.red | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index 106af3a325..96822c369f 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -61,8 +61,9 @@ system/reactivity: context [ ] ] - ++: make op! func ['word value] [set word add get word value] - peak: func ['word value] [set word max get word value] + incr: func ['word value] [set word add get word value] + peak: func ['word value] [set word max get word value] + ++: make op! :incr ;@@ workaround for #4526 register: func ['counter value] [set counter max get counter value] ] @@ -432,8 +433,8 @@ system/reactivity: context [ /later "Run the reaction on next change instead of now" /with "Specifies an optional face object (internal use)" ctx [object! set-word! none!] "Optional context for VID faces or target set-word" - /local item return: [block! function! none!] "The reactive relation or NONE if no relation was processed" + /local item ][ case [ link [ From cd91ed334fd3740a11bf02da6e3e6d4ec6a4a295 Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Wed, 17 Jun 2020 21:34:54 +0300 Subject: [PATCH 04/13] FIX: 'protected' output should be enabled for all debug modes --- environment/reactivity.red | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index 96822c369f..be142820c9 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -540,7 +540,7 @@ reactor!: context [ ][ set-quiet in self word :old ;-- force the old value sr/--measure-- [skipped ++ 1] - sr/--debug-print--/full ["-- react: protected --" word "VALUE" :old "IN" self] + sr/--debug-print-- ["-- react: protected --" word "VALUE" :old "IN" self] exit ] unless all [series? :old series? :new same? head :old head :new][ From 0f353097a206602851f519f08fc16d55acb9db9c Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Sat, 20 Jun 2020 17:38:38 +0300 Subject: [PATCH 05/13] FIX: refactored reactivity a bit --- environment/reactivity.red | 77 ++++++++++++++++++++------------------ modules/view/view.red | 2 +- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index be142820c9..cdcc5edd59 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -10,6 +10,10 @@ Red [ } ] +#local [ +#macro REL-PERIOD: func [][3] +#macro IDX-PERIOD: func [][2] + system/reactivity: context [ ;-- index format: [reaction [reactors..] ...] -- required by react/unlink 'all, dump-reactions, clear-reactions, stop-reactor index: make hash! 1000 ;-- hash speeds up unlinking noticeably @@ -61,9 +65,8 @@ system/reactivity: context [ ] ] - incr: func ['word value] [set word add get word value] + incr: func ['word] [set word 1 + get word] peak: func ['word value] [set word max get word value] - ++: make op! :incr ;@@ workaround for #4526 register: func ['counter value] [set counter max get counter value] ] @@ -108,7 +111,13 @@ system/reactivity: context [ ] ] - relations-of: func [reactor [object!]] [first body-of :reactor/on-change*] + remove-part: function [where [series!] part [integer!]] [ ;-- O(1) anywhere + change where end: skip tail where 0 - part + clear end + ] + + relations-of: func [reactor [object!]] [select body-of :reactor/on-change* 'relations] + reactors-for: func [reaction [block! function!]] [select/same/only/skip index :reaction IDX-PERIOD] unique-objects: function [] [ ;-- used by debug funcs only unique collect [foreach [_ list] index [keep list]] @@ -116,7 +125,7 @@ system/reactivity: context [ relations-count: function [] [ sum: 0 - foreach obj unique-objects [sum: (length? relations-of obj) / 3 + sum] + foreach obj unique-objects [sum: (length? relations-of obj) / REL-PERIOD + sum] sum ] @@ -129,9 +138,9 @@ system/reactivity: context [ --measure-- [time/count in-add] new-rel: head reduce/into [:word :reaction targets] clear [] relations: relations-of obj - unless find/same/skip relations new-rel 3 [ + unless find/same/skip relations new-rel REL-PERIOD [ append relations new-rel - unless objs: select/only/same/skip index :reaction 2 [ + unless objs: reactors-for :reaction [ reduce/into [:reaction objs: make block! 10] tail index ] unless find/same objs obj [append objs obj] @@ -141,9 +150,9 @@ system/reactivity: context [ ] ] --measure-- [ - peak max-relations (length? relations) / 3 + peak max-relations (length? relations) / REL-PERIOD peak max-reactors length? objs - peak max-index (length? index) / 2 + peak max-index (length? index) / IDX-PERIOD ] --debug-print-- ["-- react: added --" :reaction "FOR" word "IN" obj] ] @@ -153,7 +162,7 @@ system/reactivity: context [ eval: function [code [block!] /safe /local result saved][ --measure-- [ time/count in-eval - fired ++ 1 + incr fired ] --debug-print--/full ["-- react: firing --" either function? first code [body-of first code][code]] either safe [ @@ -186,20 +195,20 @@ system/reactivity: context [ --debug-print--/full/no-gui ["-- react: checking --" field "IN" reactor] --measure-- [time/count in-check] pos: relations-of reactor - unless pos: find/skip pos field 3 [exit] + unless pos: find/skip pos field REL-PERIOD [exit] if initial?: tail? source [reduce/into [reactor field] source] until [ set [word reaction target] pos case [ pending? reactor :reaction [ ;-- don't allow cycles - --measure-- [skipped ++ 1] + --measure-- [incr skipped] --debug-print--/no-gui ["-- react: skipped --" :reaction "FOR" field "IN" reactor] 'idle ] not tail? queue [ ;-- entered while another reaction is running reduce/into [reactor :reaction target no] tail queue --measure-- [ - queued ++ 1 + incr queued peak max-queue (length? queue) / 4 ] --debug-print--/no-gui ["-- react: queued --" :reaction "FOR" field "IN" reactor] @@ -226,7 +235,7 @@ system/reactivity: context [ --measure-- [time/count in-check] ] ] - none? pos: find/skip skip pos 3 field 3 + none? pos: find/skip skip pos REL-PERIOD field REL-PERIOD ] if initial? [clear source] --measure-- [peak longest-flush time/save] @@ -239,15 +248,12 @@ system/reactivity: context [ ][ --measure-- [time/count in-remove] relations: relations-of obj - reactions: unique extract/into next relations 3 clear [] ;-- same reaction may be repeated many times for different words + reactions: unique extract/into next relations REL-PERIOD clear [] ;-- same reaction may be repeated many times for different words foreach reaction reactions [ --debug-print-- ["-- react: removed --" :reaction "FROM" obj] - pos: find/same/only/skip index :reaction 2 + pos: find/same/only/skip index :reaction IDX-PERIOD remove find/same pos/2 obj - if tail? pos/2 [ - change pos end: skip tail pos -2 - clear end - ] + if tail? pos/2 [remove-part pos IDX-PERIOD] ] clear relations --measure-- [time/save] @@ -372,13 +378,13 @@ system/reactivity: context [ pos: relations-of reactor either target [ pos: at pos 3 - if pos: find/skip pos field 3 [reaction: :pos/-1] ;-- looks for a set-word + if pos: find/skip pos field REL-PERIOD [reaction: :pos/-1] ;-- looks for a set-word ][ - if pos: find/skip pos field 3 [reaction: :pos/2] + if pos: find/skip pos field REL-PERIOD [reaction: :pos/2] ] all [ ;-- have to verify that on-change* block wasn't just copied into this object from other reactor :reaction - objs: select/same/only/skip index :reaction 2 + objs: reactors-for :reaction find/same objs reactor return :reaction ] @@ -406,10 +412,9 @@ system/reactivity: context [ unlink-reaction: function [reactor [object!] reaction [function! block!]] [ pos: next relations-of reactor - while [pos: find/same/only/skip pos :reaction 3][ + while [pos: find/same/only/skip pos :reaction REL-PERIOD][ --debug-print-- ["-- react: removed --" :reaction "FOR" pos/-1 "IN" reactor] - change back found?: pos end: skip tail pos -3 - clear end + remove-part back found?: pos REL-PERIOD ] found? ] @@ -470,12 +475,11 @@ system/reactivity: context [ case [ object? src [ if found?: unlink-reaction src :reaction [ - objs: select/only/same/skip index :reaction 2 - remove find/same objs src + remove find/same reactors-for :reaction src ] ] block? src [ - objs: select/only/same/skip index :reaction 2 + objs: reactors-for :reaction foreach obj src [ if unlink-reaction obj :reaction [ remove find/same objs obj @@ -484,13 +488,12 @@ system/reactivity: context [ ] ] src = 'all [ - if pos: find/only/same/skip index :reaction 2 [ + if pos: find/only/same/skip index :reaction IDX-PERIOD [ foreach obj pos/2 [ if unlink-reaction obj :reaction [found?: yes] ] clear pos/2 ;-- dissociate from all objects - change pos end: skip tail pos -2 - clear end + remove-part pos IDX-PERIOD ] ] 'else [cause-error 'script 'invalid-arg [src]] @@ -518,8 +521,9 @@ system/reactivity: context [ ] ] either found? [:reaction][none] ;-- returns NONE if no relation was processed - ] -] + ];; set 'react + +];; system/reactivity reactor!: context [ on-change*: function [word old [any-type!] new [any-type!]] [ @@ -528,18 +532,18 @@ reactor!: context [ ;; src-word function [func obj1 obj2...] -- used by react/link (evaluates target), one relation for every reactor in both list and func's body ;; src-word [reaction] none -- used by react (evaluates reaction) ;; src-word [reaction] set-word/object -- used by react/with (evaluates reaction, assigns to a set-word only) - [] ;-- relations placeholder (hash is ~10x times slower) + relations: [] ;-- relations placeholder (hash is ~10x times slower) sr: system/reactivity sr/--debug-print--/full ["-- react: on-change --" word "FROM" type? :old "TO" type? :new] - sr/--measure-- [events ++ 1] + sr/--measure-- [incr events] if all [ not empty? srs: sr/source srs/1 =? self srs/2 = word ][ set-quiet in self word :old ;-- force the old value - sr/--measure-- [skipped ++ 1] + sr/--measure-- [incr skipped] sr/--debug-print-- ["-- react: protected --" word "VALUE" :old "IN" self] exit ] @@ -560,3 +564,4 @@ deep-reactor!: make reactor! [ reactor: function [spec [block!]][make reactor! spec] deep-reactor: function [spec [block!]][make deep-reactor! spec] +];; #local diff --git a/modules/view/view.red b/modules/view/view.red index f62347bbdd..5e92e03057 100644 --- a/modules/view/view.red +++ b/modules/view/view.red @@ -388,7 +388,7 @@ face!: object [ ;-- keep in sync with facet! enum draw: none on-change*: function [word old new][ - [] + relations: [] ;-- used by reactivity if debug-info? self [ print [ "-- on-change event --" lf From f6ee105a3ff06e98ba3fe736b5aee10892606203 Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Sat, 20 Jun 2020 22:42:23 +0300 Subject: [PATCH 06/13] FIX: more reliable trimming algorithm for trimming debug output --- environment/reactivity.red | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index cdcc5edd59..d2990bb963 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -98,12 +98,19 @@ system/reactivity: context [ "make object!" "object" ] ] - if 1 < overshoot: (length? s: form blk) / limit [ ;-- trim longest parts equally + if (length? s: form blk) > limit [ ;-- trim longest parts equally + long: 0 count: 0 foreach x next blk [ ;-- don't trim the prefix - if 15 < n: length? x [ - clear skip x to integer! n / overshoot + if 15 < len: length? x [ ;-- don't trim short values + long: long + len + count: count + 1 ] ] + short: (len: length? s) - long + ratio: max 0 limit - short / (len - short) + foreach x next blk [ + clear skip x max 15 to integer! ratio * length? x + ] clear skip s: form blk limit ] print s @@ -117,7 +124,7 @@ system/reactivity: context [ ] relations-of: func [reactor [object!]] [select body-of :reactor/on-change* 'relations] - reactors-for: func [reaction [block! function!]] [select/same/only/skip index :reaction IDX-PERIOD] + reactors-for: func [reaction [block! function!]] [select/same/only/skip index :reaction IDX-PERIOD] unique-objects: function [] [ ;-- used by debug funcs only unique collect [foreach [_ list] index [keep list]] From 4f53226ca2870e6a5f1e76dad8417338338868e3 Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Fri, 1 Jan 2021 19:47:55 +0300 Subject: [PATCH 07/13] FEAT: lightweight reactors by code sharing --- environment/reactivity.red | 45 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index d2990bb963..da3f06ebe6 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -248,6 +248,27 @@ system/reactivity: context [ --measure-- [peak longest-flush time/save] ] + on-change-handler: function [owner [object!] word [word! set-word!] old [any-type!] new [any-type!]] [ + sr: system/reactivity + sr/--debug-print--/full ["-- react: on-change --" word "FROM" type? :old "TO" type? :new] + sr/--measure-- [incr events] + if all [ + not empty? srs: sr/source + srs/1 =? owner + srs/2 = word + ][ + set-quiet in owner word :old ;-- force the old value + sr/--measure-- [incr skipped] + sr/--debug-print-- ["-- react: protected --" word "VALUE" :old "IN" owner] + exit + ] + unless all [series? :old series? :new same? head :old head :new][ + if any [series? :old object? :old][modify old 'owned none] + if any [series? :new object? :new][modify new 'owned reduce [owner word]] + ] + sr/check owner word + ] + set 'stop-reactor function [ "Forget all relations involving reactor OBJ" obj [object!] "Face or reactor" @@ -533,37 +554,19 @@ system/reactivity: context [ ];; system/reactivity reactor!: context [ - on-change*: function [word old [any-type!] new [any-type!]] [ + on-change*: func [word [word! set-word!] old [any-type!] new [any-type!]] [ ;-- relations format: [reactor word reaction targets] ;; src-word [reaction] set-word -- used by `is` (evaluates reaction, assigns to target) ;; src-word function [func obj1 obj2...] -- used by react/link (evaluates target), one relation for every reactor in both list and func's body ;; src-word [reaction] none -- used by react (evaluates reaction) ;; src-word [reaction] set-word/object -- used by react/with (evaluates reaction, assigns to a set-word only) relations: [] ;-- relations placeholder (hash is ~10x times slower) - - sr: system/reactivity - sr/--debug-print--/full ["-- react: on-change --" word "FROM" type? :old "TO" type? :new] - sr/--measure-- [incr events] - if all [ - not empty? srs: sr/source - srs/1 =? self - srs/2 = word - ][ - set-quiet in self word :old ;-- force the old value - sr/--measure-- [incr skipped] - sr/--debug-print-- ["-- react: protected --" word "VALUE" :old "IN" self] - exit - ] - unless all [series? :old series? :new same? head :old head :new][ - if any [series? :old object? :old][modify old 'owned none] - if any [series? :new object? :new][modify new 'owned reduce [self word]] - ] - sr/check self word + system/reactivity/on-change-handler context? word word :old :new ] ] deep-reactor!: make reactor! [ - on-deep-change*: function [owner word target action new [any-type!] index part][ + on-deep-change*: func [owner word target action new [any-type!] index part][ system/reactivity/check owner word ] ] From 59d8b7d8312516487e623d1d198536a7551cd81c Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Fri, 1 Jan 2021 20:26:16 +0300 Subject: [PATCH 08/13] FIX: a bit simpler on-change* --- environment/reactivity.red | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index da3f06ebe6..a5fea8d3e7 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -561,7 +561,7 @@ reactor!: context [ ;; src-word [reaction] none -- used by react (evaluates reaction) ;; src-word [reaction] set-word/object -- used by react/with (evaluates reaction, assigns to a set-word only) relations: [] ;-- relations placeholder (hash is ~10x times slower) - system/reactivity/on-change-handler context? word word :old :new + system/reactivity/on-change-handler self word :old :new ] ] From 4ba175c2aae0140e1af6690648ad96c85b011e4f Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Tue, 1 Jun 2021 14:09:50 +0300 Subject: [PATCH 09/13] FEAT: rewritten using central reactor storage, akin to original design --- environment/reactivity.red | 224 ++++++++++++++----------- tests/source/units/reactivity-test.red | 6 +- 2 files changed, 126 insertions(+), 104 deletions(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index a5fea8d3e7..15c1492dc2 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -1,22 +1,32 @@ Red [ Title: "Reactive programming support" - Author: "Nenad Rakocevic" + Author: ["Nenad Rakocevic" "hiiamboris"] File: %reactivity.red Tabs: 4 - Rights: "Copyright (C) 2016-2020 Red Foundation. All rights reserved." + Rights: "Copyright (C) 2016-2021 Red Foundation. All rights reserved." License: { Distributed under the Boost Software License, Version 1.0. See https://github.com/red/red/blob/master/BSL-License.txt } ] -#local [ -#macro REL-PERIOD: func [][3] -#macro IDX-PERIOD: func [][2] +#local [ ;-- keep macros from flowing out +#macro REACTORS-PERIOD: func [][2] +#macro RELATIONS-PERIOD: func [][3] +#macro REACTIONS-PERIOD: func [][2] system/reactivity: context [ - ;-- index format: [reaction [reactors..] ...] -- required by react/unlink 'all, dump-reactions, clear-reactions, stop-reactor - index: make hash! 1000 ;-- hash speeds up unlinking noticeably + ;-- reactors format: [reactor [records] ...] + ;-- each record format: [reactor word reaction targets] - a block (hash is ~10x times slower) + ;; src-word [reaction] set-word -- used by `is` (evaluates reaction, assigns to target) + ;; src-word function [func obj1 obj2...] -- used by react/link (evaluates target), one relation for every reactor in both list and func's body + ;; src-word [reaction] none -- used by react (evaluates reaction) + ;; src-word [reaction] set-word/object -- used by react/with (evaluates reaction, assigns to a set-word only) + reactors: make hash! 500 + + ;-- reactions format: [reaction [reactors..] ...] + ;-- (used by react/unlink 'all, dump-reactions, clear-reactions, stop-reactor) + reactions: make hash! 1000 ;-- hash speeds up unlinking noticeably ;-- queue format: [reactor reaction target done] queue: make hash! 100 @@ -26,7 +36,7 @@ system/reactivity: context [ source: [] ;-- contains the initial [reactor reaction] that triggered a chain of subsequent reactions metrics: context [ - max-queue: max-index: max-reactors: max-relations: 0 + max-queue: max-reactions: max-reactors: biggest-relation: biggest-reactor: 0 events: fired: queued: skipped: 0 in-add: in-remove: in-react: in-check: in-eval: longest-flush: 0:0 @@ -50,10 +60,11 @@ system/reactivity: context [ print [" dispatching: " in-check] print [" longest queue flush:" longest-flush] print "Peak values:" - print [" maximum queue size: " max-queue] - print [" maximum index size: " max-index] - print [" biggest relation: " max-reactors "reactors"] - print [" most used reactor: " max-relations "relations"] + print [" max queue size: " max-queue] + print [" max reactors count: " max-reactors] + print [" max reactions count:" max-reactions] + print [" biggest relation: " biggest-relation "reactors"] + print [" biggest reactor: " biggest-reactor "relations"] ] start: target: none @@ -123,16 +134,29 @@ system/reactivity: context [ clear end ] - relations-of: func [reactor [object!]] [select body-of :reactor/on-change* 'relations] - reactors-for: func [reaction [block! function!]] [select/same/only/skip index :reaction IDX-PERIOD] + find-reactor: func [reactor [object!]] [ + find/same/skip reactors reactor REACTORS-PERIOD + ] + relations-of: func [reactor [object!]] [ + select/same/skip reactors reactor REACTORS-PERIOD + ] - unique-objects: function [] [ ;-- used by debug funcs only - unique collect [foreach [_ list] index [keep list]] + find-reaction: func [reaction [block! function!]] [ + find/same/only/skip reactions :reaction REACTIONS-PERIOD + ] + reactors-for: func [reaction [block! function!]] [ + select/same/only/skip reactions :reaction REACTIONS-PERIOD + ] + + unique-objects: func [] [ ;-- used by debug funcs only + extract reactors 2 ] relations-count: function [] [ sum: 0 - foreach obj unique-objects [sum: (length? relations-of obj) / REL-PERIOD + sum] + foreach obj unique-objects [ + sum: (length? relations-of obj) / RELATIONS-PERIOD + sum + ] sum ] @@ -144,39 +168,40 @@ system/reactivity: context [ ][ --measure-- [time/count in-add] new-rel: head reduce/into [:word :reaction targets] clear [] - relations: relations-of obj - unless find/same/skip relations new-rel REL-PERIOD [ + either relations: relations-of obj [ + found: find/same/skip relations new-rel RELATIONS-PERIOD + ][ + reduce/into [obj relations: make block! 12] tail reactors + ] + unless found [ append relations new-rel unless objs: reactors-for :reaction [ - reduce/into [:reaction objs: make block! 10] tail index - ] - unless find/same objs obj [append objs obj] - if block? targets [ ;-- react/link func .. [func objects..] case - foreach obj next targets [ - unless find/same objs obj [append objs obj] - ] + reduce/into [:reaction objs: make block! 10] tail reactions ] + unless find/same objs obj [append objs obj] ;-- only source object should be kept, not targets --measure-- [ - peak max-relations (length? relations) / REL-PERIOD - peak max-reactors length? objs - peak max-index (length? index) / IDX-PERIOD + peak biggest-reactor (length? relations) / RELATIONS-PERIOD + peak biggest-relation length? objs + peak max-reactions (length? reactions) / REACTIONS-PERIOD + peak max-reactors (length? reactors) / REACTORS-PERIOD ] --debug-print-- ["-- react: added --" :reaction "FOR" word "IN" obj] ] --measure-- [time/save] ] - eval: function [code [block!] /safe /local result saved][ + eval: function [code [block!] /safe /local result][ --measure-- [ time/count in-eval incr fired ] - --debug-print--/full ["-- react: firing --" either function? first code [body-of first code][code]] + --debug-print--/full ["-- react: firing --" either function? :code/1 [body-of :code/1][code]] either safe [ if error? error: try/all [set/any 'result do code 'ok] [ print :error prin "*** Near: " - print mold/part/flat code 80 + limit: (any [all [system/console system/console/size/x] 72]) - 11 + print mold/part/flat code limit ] ][ set/any 'result do code @@ -194,18 +219,19 @@ system/reactivity: context [ ] pending?: function [reactor [object!] reaction [block! function!]][ - pattern: head reduce/into [reactor :reaction] clear [] + reduce/into [reactor :reaction] clear pattern: [] none <> find/same/skip queue pattern 4 ] - check: function [reactor [object!] field [word! set-word!] /local word reaction target][ + check: function [reactor [object!] field [word! set-word!]][ + unless pos: relations-of reactor [exit] ;-- immediate return for reactors without defined relations --debug-print--/full/no-gui ["-- react: checking --" field "IN" reactor] --measure-- [time/count in-check] - pos: relations-of reactor - unless pos: find/skip pos field REL-PERIOD [exit] + unless pos: find/skip pos field RELATIONS-PERIOD [exit] + if initial?: tail? source [reduce/into [reactor field] source] until [ - set [word reaction target] pos + set [word: reaction: target:] pos case [ pending? reactor :reaction [ ;-- don't allow cycles --measure-- [incr skipped] @@ -242,31 +268,35 @@ system/reactivity: context [ --measure-- [time/count in-check] ] ] - none? pos: find/skip skip pos REL-PERIOD field REL-PERIOD + none? pos: find/skip (skip pos RELATIONS-PERIOD) field RELATIONS-PERIOD ] if initial? [clear source] --measure-- [peak longest-flush time/save] ] - + + ;-- kept separately to minimize reactor's RAM size on-change-handler: function [owner [object!] word [word! set-word!] old [any-type!] new [any-type!]] [ - sr: system/reactivity - sr/--debug-print--/full ["-- react: on-change --" word "FROM" type? :old "TO" type? :new] - sr/--measure-- [incr events] + --debug-print--/full ["-- react: on-change --" word "FROM" type? :old "TO" type? :new] + --measure-- [incr events] if all [ - not empty? srs: sr/source - srs/1 =? owner - srs/2 = word + not empty? source + source/1 =? owner + source/2 = word ][ set-quiet in owner word :old ;-- force the old value - sr/--measure-- [incr skipped] - sr/--debug-print-- ["-- react: protected --" word "VALUE" :old "IN" owner] + --measure-- [incr skipped] + --debug-print-- ["-- react: protected --" word "VALUE" :old "IN" owner] exit ] - unless all [series? :old series? :new same? head :old head :new][ - if any [series? :old object? :old][modify old 'owned none] - if any [series? :new object? :new][modify new 'owned reduce [owner word]] + all [ + in owner 'on-deep-change* ;-- only deep reactors take ownership + not all [series? :old series? :new same? head :old head :new] + case/all [ + any [series? :old object? :old] [modify old 'owned none] + any [series? :new object? :new] [modify new 'owned reduce [owner word]] + ] ] - sr/check owner word + check owner word ] set 'stop-reactor function [ @@ -274,16 +304,17 @@ system/reactivity: context [ obj [object!] "Face or reactor" /deep "Deeply remove all relations from child faces" ][ + unless found: find-reactor obj [exit] --measure-- [time/count in-remove] - relations: relations-of obj - reactions: unique extract/into next relations REL-PERIOD clear [] ;-- same reaction may be repeated many times for different words - foreach reaction reactions [ + relations: found/2 + obj-reacs: unique extract/into next relations RELATIONS-PERIOD clear [] ;-- same reaction may be repeated many times for different words + foreach reaction obj-reacs [ --debug-print-- ["-- react: removed --" :reaction "FROM" obj] - pos: find/same/only/skip index :reaction IDX-PERIOD + pos: find-reaction :reaction remove find/same pos/2 obj - if tail? pos/2 [remove-part pos IDX-PERIOD] + if tail? pos/2 [remove-part pos REACTIONS-PERIOD] ] - clear relations + remove-part found REACTORS-PERIOD --measure-- [time/save] if all [deep block? :obj/pane] [ @@ -292,12 +323,9 @@ system/reactivity: context [ ] set 'clear-reactions function ["Remove all reactive relations"][ - foreach obj unique-objects [ - --debug-print-- ["-- react: clearing all relations -- FROM" obj] - relations: relations-of obj - clear relations - ] - clear index + --debug-print-- ["-- react: clearing ALL relations --"] + clear reactions + clear reactors ] set 'dump-reactions function [ @@ -306,8 +334,8 @@ system/reactivity: context [ limit: (any [all [system/console system/console/size/x] 72]) - 10 count: 0 - foreach reactor unique-objects [ - foreach [field reaction target] relations-of reactor [ + foreach [reactor relations] reactors [ + foreach [field reaction target] relations [ prin count: count + 1 prin ":---^/" prin " Source: object " @@ -401,22 +429,16 @@ system/reactivity: context [ reactor [object!] "Object to check" field [word!] "Field to check" /target "Check if it's a target instead of a source" - return: [block! function! word! none!] "Returns reaction, type or NONE" + ; return: [block! function! word! none!] "Returns reaction, type or NONE" ][ - pos: relations-of reactor - either target [ - pos: at pos 3 - if pos: find/skip pos field REL-PERIOD [reaction: :pos/-1] ;-- looks for a set-word - ][ - if pos: find/skip pos field REL-PERIOD [reaction: :pos/2] - ] - all [ ;-- have to verify that on-change* block wasn't just copied into this object from other reactor - :reaction - objs: reactors-for :reaction - find/same objs reactor - return :reaction + if pos: relations-of reactor [ + either target [ + pos: at pos 3 + if pos: find/skip pos field RELATIONS-PERIOD [:pos/-1] ;-- looks for a set-word + ][ + if pos: find/skip pos field RELATIONS-PERIOD [:pos/2] + ] ] - none ] get-object-path-length: function [path [any-path!] obj [word!]] [ @@ -439,20 +461,30 @@ system/reactivity: context [ ] unlink-reaction: function [reactor [object!] reaction [function! block!]] [ - pos: next relations-of reactor - while [pos: find/same/only/skip pos :reaction REL-PERIOD][ + unless at-reactor: find-reactor reactor [return none] + pos: next relations: at-reactor/2 + while [pos: find/same/only/skip pos :reaction RELATIONS-PERIOD] [ --debug-print-- ["-- react: removed --" :reaction "FOR" pos/-1 "IN" reactor] - remove-part back found?: pos REL-PERIOD + remove-part back found?: pos RELATIONS-PERIOD ] + if empty? relations [remove-part at-reactor REACTORS-PERIOD] ;-- don't lock it from GC + + at-reaction: find-reaction :reaction + remove find/same objs: at-reaction/2 reactor + remove find/same at-reaction/2 reactor + remove find/same at-reaction/2 reactor + if empty? objs [remove-part at-reaction REACTIONS-PERIOD] ;-- don't lock it from GC found? ] + ;-- used by `react` to determine valid reactive sources (should also support custom ones) + ;-- which are: objects that define on-change* and eventually call `check` + ;-- but last condition can't be verified, because check may be buried in other function calls, so.. set 'reactor? function ["Check if object is a reactor" obj [any-type!]] [ all [ object? :obj oc: in obj 'on-change* function? get/any oc ;-- can be unset when `is` is used in global context - any-block? relations-of obj ;-- is a reactor, not other object with on-change* ] ] @@ -466,7 +498,7 @@ system/reactivity: context [ /later "Run the reaction on next change instead of now" /with "Specifies an optional face object (internal use)" ctx [object! set-word! none!] "Optional context for VID faces or target set-word" - return: [block! function! none!] "The reactive relation or NONE if no relation was processed" + ; return: [block! function! none!] "The reactive relation or NONE if no relation was processed" /local item ][ case [ @@ -502,26 +534,20 @@ system/reactivity: context [ --measure-- [time/count in-remove] case [ object? src [ - if found?: unlink-reaction src :reaction [ - remove find/same reactors-for :reaction src - ] + found?: unlink-reaction src :reaction ] block? src [ objs: reactors-for :reaction foreach obj src [ - if unlink-reaction obj :reaction [ - remove find/same objs obj - found?: yes - ] + if unlink-reaction obj :reaction [found?: yes] ] ] src = 'all [ - if pos: find/only/same/skip index :reaction IDX-PERIOD [ - foreach obj pos/2 [ - if unlink-reaction obj :reaction [found?: yes] + if pos: find-reaction :reaction [ + objs: pos/2 ;-- save it; unlink may change `pos` content + while [not empty? objs] [ + if unlink-reaction objs/1 :reaction [found?: yes] ] - clear pos/2 ;-- dissociate from all objects - remove-part pos IDX-PERIOD ] ] 'else [cause-error 'script 'invalid-arg [src]] @@ -555,12 +581,6 @@ system/reactivity: context [ reactor!: context [ on-change*: func [word [word! set-word!] old [any-type!] new [any-type!]] [ - ;-- relations format: [reactor word reaction targets] - ;; src-word [reaction] set-word -- used by `is` (evaluates reaction, assigns to target) - ;; src-word function [func obj1 obj2...] -- used by react/link (evaluates target), one relation for every reactor in both list and func's body - ;; src-word [reaction] none -- used by react (evaluates reaction) - ;; src-word [reaction] set-word/object -- used by react/with (evaluates reaction, assigns to a set-word only) - relations: [] ;-- relations placeholder (hash is ~10x times slower) system/reactivity/on-change-handler self word :old :new ] ] diff --git a/tests/source/units/reactivity-test.red b/tests/source/units/reactivity-test.red index 991a867581..3ceb903c59 100644 --- a/tests/source/units/reactivity-test.red +++ b/tests/source/units/reactivity-test.red @@ -7,6 +7,8 @@ Red [ License: "BSD-3 - https://github.com/red/red/blob/origin/BSD-3-License.txt" ] + +;#include %../../../environment/reactivity.red #include %../../../quick-test/quick-test.red ~~~start-file~~~ "reactivity" @@ -154,7 +156,7 @@ Red [ rf-6r: make reactor! [x: 1] rf-6c: context [ x: is [rf-6r/x] ] --assert 1 = system/reactivity/relations-count ;-- should only be a single reaction - --assert rf-6r =? :system/reactivity/index/2/1 ;-- `rf-6r` should be the source object + --assert rf-6r =? :system/reactivity/reactions/2/1 ;-- `rf-6r` should be the source object unset [rf-6c rf-6r] --test-- "rf-7" ;-- #3333 triple-reaction case @@ -162,7 +164,7 @@ Red [ rf-7r: make reactor! [x: 1] rf-7x: is [rf-7r/x] --assert 1 = system/reactivity/relations-count ;-- should only be a single reaction - --assert rf-7r = :system/reactivity/index/2/1 ;-- `rf-7r` should be the source object + --assert rf-7r = :system/reactivity/reactions/2/1 ;-- `rf-7r` should be the source object unset [rf-7r rf-7x] --test-- "rf-8" ;-- correct path recognition From 59b09d5b46baebf4c4257d169a509b4a41b4a64b Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Tue, 1 Jun 2021 15:54:00 +0300 Subject: [PATCH 10/13] FIX: hide reactor?, get back return spec --- environment/reactivity.red | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index 15c1492dc2..a4451b93e9 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -429,7 +429,7 @@ system/reactivity: context [ reactor [object!] "Object to check" field [word!] "Field to check" /target "Check if it's a target instead of a source" - ; return: [block! function! word! none!] "Returns reaction, type or NONE" + return: [block! function! word! none!] "Returns reaction, type or NONE" ][ if pos: relations-of reactor [ either target [ @@ -480,7 +480,7 @@ system/reactivity: context [ ;-- used by `react` to determine valid reactive sources (should also support custom ones) ;-- which are: objects that define on-change* and eventually call `check` ;-- but last condition can't be verified, because check may be buried in other function calls, so.. - set 'reactor? function ["Check if object is a reactor" obj [any-type!]] [ + reactor?: function ["Check if object is a reactor" obj [any-type!]] [ all [ object? :obj oc: in obj 'on-change* @@ -498,7 +498,7 @@ system/reactivity: context [ /later "Run the reaction on next change instead of now" /with "Specifies an optional face object (internal use)" ctx [object! set-word! none!] "Optional context for VID faces or target set-word" - ; return: [block! function! none!] "The reactive relation or NONE if no relation was processed" + return: [block! function! none!] "The reactive relation or NONE if no relation was processed" /local item ][ case [ From 830851b9a95d7d2b8f5d3c5caae7b886981a7347 Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Tue, 1 Jun 2021 16:35:35 +0300 Subject: [PATCH 11/13] TESTS: workaround for #1706 --- tests/source/units/reactivity-test.red | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/source/units/reactivity-test.red b/tests/source/units/reactivity-test.red index 3ceb903c59..a65c39f3df 100644 --- a/tests/source/units/reactivity-test.red +++ b/tests/source/units/reactivity-test.red @@ -223,8 +223,10 @@ Red [ --assert a3091/c = 2 --test-- "#4022" - a4022: make reactor! [i: repeat i 2 [i]] - --assert a4022/i = 2 + do [ ;@@ FIXME: compiler can't handle this - see comments in #1706 + a4022: make reactor! [i: repeat i 2 [i]] + --assert a4022/i = 2 + ] --test-- "#4166-1" r4166: make reactor! [x: y: 1] From 68f213afd0990591f5e10da0de5a6bf2e55b89ea Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Tue, 1 Jun 2021 16:54:45 +0300 Subject: [PATCH 12/13] FIX: removed leftover debug exprs --- environment/reactivity.red | 2 -- 1 file changed, 2 deletions(-) diff --git a/environment/reactivity.red b/environment/reactivity.red index a4451b93e9..6b271dea6f 100644 --- a/environment/reactivity.red +++ b/environment/reactivity.red @@ -471,8 +471,6 @@ system/reactivity: context [ at-reaction: find-reaction :reaction remove find/same objs: at-reaction/2 reactor - remove find/same at-reaction/2 reactor - remove find/same at-reaction/2 reactor if empty? objs [remove-part at-reaction REACTIONS-PERIOD] ;-- don't lock it from GC found? ] From 3f8216eb16ae3af4a6f44d7a6f227bc9eee32a12 Mon Sep 17 00:00:00 2001 From: hiiamboris Date: Tue, 1 Jun 2021 16:56:42 +0300 Subject: [PATCH 13/13] FIX: removed leftover include --- tests/source/units/reactivity-test.red | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/source/units/reactivity-test.red b/tests/source/units/reactivity-test.red index a65c39f3df..bf749affea 100644 --- a/tests/source/units/reactivity-test.red +++ b/tests/source/units/reactivity-test.red @@ -7,8 +7,6 @@ Red [ License: "BSD-3 - https://github.com/red/red/blob/origin/BSD-3-License.txt" ] - -;#include %../../../environment/reactivity.red #include %../../../quick-test/quick-test.red ~~~start-file~~~ "reactivity"