From 4da18f946307e35cf145bd6e2d42d382276ff3a6 Mon Sep 17 00:00:00 2001 From: Dierk Koenig Date: Sun, 8 Dec 2024 19:43:32 +0100 Subject: [PATCH] observable: add log warning on suspicious listener count to shield against memory leak. --- docs/features.html | 114 ----------------------------- docs/src/kolibri/observable.js | 57 ++++++++++----- docs/src/kolibri/observableTest.js | 25 ++++++- 3 files changed, 59 insertions(+), 137 deletions(-) delete mode 100644 docs/features.html diff --git a/docs/features.html b/docs/features.html deleted file mode 100644 index ea0b5f31..00000000 --- a/docs/features.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - Kolibri Features - - - - - - -
-
- - - - - - - - - - -
-

Kolibri

-
Features
-
- -
- -
-

Standard Library

-

- Immutable: Pair, Tuple, Maybe, Either, Sequence, JINQ, JSON monad - along with many church-encoded lambda abstractions. -

-

- Observable, ObservableList, DataFlowVariable, Scheduler, Attribute, ModelWorld. -

-

- Utilities: array extensions, DOM handling, test facility (incl. async testing), logging, REST calls. -

-
-
-

Patterns

-

- Classical MVC, Projector Pattern -

- -
- - -
- - - diff --git a/docs/src/kolibri/observable.js b/docs/src/kolibri/observable.js index 17d36e7d..09cc59c4 100644 --- a/docs/src/kolibri/observable.js +++ b/docs/src/kolibri/observable.js @@ -1,8 +1,18 @@ - -import "./util/array.js" +import "./util/array.js"; +import { LoggerFactory } from "./logger/loggerFactory.js"; +import { LOG_CONTEXT_KOLIBRI_BASE } from "./logger/logConstants.js"; export {Observable, ObservableList} +const { warn } = LoggerFactory(LOG_CONTEXT_KOLIBRI_BASE + ".observable"); + +/** @private */ +function checkWarning(list) { + if (list.length > 100) { + warn(`Beware of memory leak. ${list.length} listeners.`); + } +} + /** * @typedef { <_T_> (newValue:_T_, oldValue: ?_T_) => void } ValueChangeCallback<_T_> * This is a specialized {@link ConsumerType} with an optional second value. @@ -37,26 +47,27 @@ export {Observable, ObservableList} * obs.onChange(val => console.log(val)); * obs.setValue("some other value"); // will be logged */ -const Observable = value => { +function Observable(value) { const listeners = []; return { onChange: callback => { + checkWarning(listeners); listeners.push(callback); callback(value, value); }, - getValue: () => value, + getValue: () => value, setValue: newValue => { if (value === newValue) return; const oldValue = value; - value = newValue; + value = newValue; listeners.forEach(callback => { if (value === newValue) { // pre-ordered listeners might have changed this and thus the callback no longer applies callback(value, oldValue); } }); } - } -}; + }; +} /** * IObservableList<_T_> is the interface for lists that can be observed for add or delete operations. @@ -87,27 +98,33 @@ const Observable = value => { * list.onAdd( item => console.log(item)); * list.add(1); */ -const ObservableList = list => { - const addListeners = []; - const delListeners = []; +function ObservableList(list) { + const addListeners = []; + const delListeners = []; const removeAddListener = addListener => addListeners.removeItem(addListener); const removeDeleteListener = delListener => delListeners.removeItem(delListener); return { - onAdd: listener => addListeners.push(listener), - onDel: listener => delListeners.push(listener), - add: item => { + onAdd: listener => { + checkWarning(addListeners); + addListeners.push(listener); + }, + onDel: listener => { + checkWarning(delListeners); + delListeners.push(listener); + }, + add: item => { list.push(item); - addListeners.forEach( listener => listener(item)) + addListeners.forEach(listener => listener(item)); }, - del: item => { + del: item => { list.removeItem(item); const safeIterate = [...delListeners]; // shallow copy as we might change the listeners array while iterating - safeIterate.forEach( listener => listener(item, () => removeDeleteListener(listener) )); + safeIterate.forEach(listener => listener(item, () => removeDeleteListener(listener))); }, removeAddListener, removeDeleteListener, - count: () => list.length, - countIf: pred => list.reduce( (sum, item) => pred(item) ? sum + 1 : sum, 0) - } -}; + count: () => list.length, + countIf: pred => list.reduce((sum, item) => pred(item) ? sum + 1 : sum, 0) + }; +} diff --git a/docs/src/kolibri/observableTest.js b/docs/src/kolibri/observableTest.js index 5e9990a9..96b19e0a 100644 --- a/docs/src/kolibri/observableTest.js +++ b/docs/src/kolibri/observableTest.js @@ -1,7 +1,10 @@ -import {Observable, ObservableList} from "./observable.js" -import "./util/array.js" -import {TestSuite} from "./util/test.js"; +import {Observable, ObservableList} from "./observable.js" +import {TestSuite} from "./util/test.js"; +import {Walk} from "./sequence/constructors/range/range.js"; +import {withDebugTestArrayAppender} from "./logger/loggerTest.js"; +import {setLoggingContext, setLoggingLevel} from "./logger/logging.js"; +import {LOG_WARN} from "./logger/logLevel.js"; const observableSuite = TestSuite("observable"); @@ -104,4 +107,20 @@ observableSuite.add("list", assert => { }); +observableSuite.add("memory leak warning", assert => { + const obs = Observable( 0 ); // decorator pattern + + withDebugTestArrayAppender(appender => { + setLoggingLevel(LOG_WARN); + setLoggingContext("ch.fhnw.kolibri.observable"); + + Walk(100).forEach$( _n => obs.onChange( _x => undefined) ); + obs.onChange( _x => undefined); // one to many + + assert.is(appender.getValue()[0], 'Beware of memory leak. 101 listeners.'); + }); + + assert.is(obs.getValue(), 0); +}); + observableSuite.run();