Skip to content

Commit

Permalink
observable: add log warning on suspicious listener count to shield ag…
Browse files Browse the repository at this point in the history
…ainst memory leak.
  • Loading branch information
Dierk Koenig committed Dec 8, 2024
1 parent 15c3771 commit 4da18f9
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 137 deletions.
114 changes: 0 additions & 114 deletions docs/features.html

This file was deleted.

57 changes: 37 additions & 20 deletions docs/src/kolibri/observable.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
};
}
25 changes: 22 additions & 3 deletions docs/src/kolibri/observableTest.js
Original file line number Diff line number Diff line change
@@ -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");

Expand Down Expand Up @@ -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();

0 comments on commit 4da18f9

Please sign in to comment.