mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 13:04:30 -08:00
finishing record io
This commit is contained in:
@@ -52,7 +52,11 @@ const apply_eval_result = (state, eval_result) => {
|
|||||||
log_position: null
|
log_position: null
|
||||||
},
|
},
|
||||||
modules: eval_result.modules,
|
modules: eval_result.modules,
|
||||||
io_cache: eval_result.io_cache,
|
io_cache:
|
||||||
|
(eval_result.io_cache == null || eval_result.io_cache.length == 0)
|
||||||
|
// If new cache is empty, reuse previous cache
|
||||||
|
? state.io_cache
|
||||||
|
: eval_result.io_cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,7 +772,6 @@ const on_deferred_call = (state, call, calltree_changed_token, logs) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO test
|
|
||||||
const clear_io_cache = state => {
|
const clear_io_cache = state => {
|
||||||
return run_code({...state, io_cache: null})
|
return run_code({...state, io_cache: null})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,6 @@ export class CallTree {
|
|||||||
this.ui.editor.focus()
|
this.ui.editor.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO test
|
|
||||||
if(e.key == 'F3') {
|
|
||||||
this.ui.set_active_tab('logs')
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if(e.key == 'a') {
|
if(e.key == 'a') {
|
||||||
if(FLAGS.embed_value_explorer) {
|
if(FLAGS.embed_value_explorer) {
|
||||||
exec('calltree.select_arguments')
|
exec('calltree.select_arguments')
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import {header, stringify_for_header} from './value_explorer.js'
|
|||||||
import {el} from './domutils.js'
|
import {el} from './domutils.js'
|
||||||
import {has_error} from '../calltree.js'
|
import {has_error} from '../calltree.js'
|
||||||
|
|
||||||
// TODO render grey items there were not used in run
|
|
||||||
|
|
||||||
export class IO_Cache {
|
export class IO_Cache {
|
||||||
constructor(ui, el) {
|
constructor(ui, el) {
|
||||||
this.el = el
|
this.el = el
|
||||||
@@ -22,15 +20,39 @@ export class IO_Cache {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render_io_cache(items) {
|
clear() {
|
||||||
this.el.innerHTML = ''
|
this.el.innerHTML = ''
|
||||||
for(let item of items) {
|
this.is_rendered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
render_io_cache(state, force) {
|
||||||
|
if(force) {
|
||||||
|
this.is_rendered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.is_rendered) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.is_rendered = true
|
||||||
|
|
||||||
|
this.el.innerHTML = ''
|
||||||
|
|
||||||
|
const items = state.io_cache ?? []
|
||||||
|
// Number of items that were used during execution
|
||||||
|
const used_count = state.eval_cxt.io_cache_index ?? items.length
|
||||||
|
|
||||||
|
for(let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i]
|
||||||
if(item.type == 'resolution') {
|
if(item.type == 'resolution') {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
const is_used = i < used_count
|
||||||
this.el.appendChild(
|
this.el.appendChild(
|
||||||
el('div',
|
el('div',
|
||||||
'call_header ' + (has_error(item) ? 'error' : ''),
|
'call_header '
|
||||||
|
+ (has_error(item) ? 'error ' : '')
|
||||||
|
+ (is_used ? '' : 'native '),
|
||||||
item.name,
|
item.name,
|
||||||
'(' ,
|
'(' ,
|
||||||
// TODO fn_link, like in ./calltree.js
|
// TODO fn_link, like in ./calltree.js
|
||||||
|
|||||||
@@ -22,12 +22,6 @@ export class Logs {
|
|||||||
this.ui.editor.focus_value_explorer(this.el)
|
this.ui.editor.focus_value_explorer(this.el)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO test
|
|
||||||
if(e.key == 'F2') {
|
|
||||||
this.ui.set_active_tab('calltree')
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if(e.key == 'F3') {
|
if(e.key == 'F3') {
|
||||||
this.ui.editor.focus()
|
this.ui.editor.focus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,11 @@ export class UI {
|
|||||||
this.tabs[tab_id].classList.add('active')
|
this.tabs[tab_id].classList.add('active')
|
||||||
Object.values(this.debugger).forEach(el => el.style.display = 'none')
|
Object.values(this.debugger).forEach(el => el.style.display = 'none')
|
||||||
this.debugger[tab_id].style.display = 'block'
|
this.debugger[tab_id].style.display = 'block'
|
||||||
|
|
||||||
|
if(tab_id == 'io_cache') {
|
||||||
|
this.io_cache.render_io_cache(get_state(), false)
|
||||||
|
}
|
||||||
|
|
||||||
if(!skip_focus) {
|
if(!skip_focus) {
|
||||||
this.debugger[tab_id].focus()
|
this.debugger[tab_id].focus()
|
||||||
}
|
}
|
||||||
@@ -304,12 +309,16 @@ export class UI {
|
|||||||
|
|
||||||
this.calltree.render_calltree(state)
|
this.calltree.render_calltree(state)
|
||||||
this.logs.render_logs(null, state.logs)
|
this.logs.render_logs(null, state.logs)
|
||||||
|
}
|
||||||
|
|
||||||
// render lazily
|
render_io_cache(state) {
|
||||||
// TODO
|
// render lazily, only if selected
|
||||||
//if(this.active_tab == 'io_cache') {
|
if(this.active_tab == 'io_cache') {
|
||||||
this.io_cache.render_io_cache(state.io_cache)
|
this.io_cache.render_io_cache(state, true)
|
||||||
//}
|
} else {
|
||||||
|
// Do not render until user switch to the tab
|
||||||
|
this.io_cache.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_problems(problems) {
|
render_problems(problems) {
|
||||||
|
|||||||
9
src/effects.js
vendored
9
src/effects.js
vendored
@@ -225,7 +225,16 @@ export const render_common_side_effects = (prev, next, command, ui) => {
|
|||||||
clear_coloring(ui)
|
clear_coloring(ui)
|
||||||
render_coloring(ui, next)
|
render_coloring(ui, next)
|
||||||
ui.logs.rerender_logs(next.logs)
|
ui.logs.rerender_logs(next.logs)
|
||||||
|
|
||||||
|
if(
|
||||||
|
prev.io_cache != next.io_cache
|
||||||
|
||
|
||||||
|
prev.eval_cxt?.io_cache_index != next.eval_cxt.io_cache_index
|
||||||
|
) {
|
||||||
|
ui.render_io_cache(next)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if(get_deferred_calls(prev) == null && get_deferred_calls(next) != null) {
|
if(get_deferred_calls(prev) == null && get_deferred_calls(next) != null) {
|
||||||
|
|||||||
@@ -519,6 +519,7 @@ const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
|||||||
const value = children.reduce(
|
const value = children.reduce(
|
||||||
(arr, el) => {
|
(arr, el) => {
|
||||||
if(el.type == 'spread') {
|
if(el.type == 'spread') {
|
||||||
|
// TODO check if iterable and throw error
|
||||||
return [...arr, ...el.children[0].result.value]
|
return [...arr, ...el.children[0].result.value]
|
||||||
} else {
|
} else {
|
||||||
return [...arr, el.result.value]
|
return [...arr, el.result.value]
|
||||||
|
|||||||
116
src/record_io.js
116
src/record_io.js
@@ -1,7 +1,5 @@
|
|||||||
import {set_record_call} from './runtime.js'
|
import {set_record_call} from './runtime.js'
|
||||||
|
|
||||||
// TODO remove all console.log
|
|
||||||
|
|
||||||
const get_object_to_patch = (cxt, path) => {
|
const get_object_to_patch = (cxt, path) => {
|
||||||
let obj = cxt.window
|
let obj = cxt.window
|
||||||
for(let i = 0; i < path.length - 1; i++) {
|
for(let i = 0; i < path.length - 1; i++) {
|
||||||
@@ -22,33 +20,28 @@ const io_patch = (cxt, path, use_context = false) => {
|
|||||||
|
|
||||||
const original = obj[method]
|
const original = obj[method]
|
||||||
obj[method] = function(...args) {
|
obj[method] = function(...args) {
|
||||||
// TODO if called from previous version of code (calltree_changed_token is
|
// TODO if called from prev execution, then throw to finish it
|
||||||
// different), then do not call IO function and throw error to finish
|
// ASAP
|
||||||
// previous run ASAP
|
|
||||||
|
|
||||||
// TODO remove
|
|
||||||
/*
|
|
||||||
console.error('patched method', name, {
|
|
||||||
io_cache_is_recording: cxt.io_cache_is_recording,
|
|
||||||
io_cache_is_replay_aborted: cxt.io_cache_is_replay_aborted,
|
|
||||||
io_cache_index: cxt.io_cache_is_recording
|
|
||||||
? cxt.io_cache.length
|
|
||||||
: cxt.io_cache_index
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
if(cxt.io_cache_is_replay_aborted) {
|
if(cxt.io_cache_is_replay_aborted) {
|
||||||
// Try to finish fast
|
// Try to finish fast
|
||||||
throw new Error('io replay aborted')
|
throw new Error('io replay aborted')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const has_new_target = new.target != null
|
||||||
|
|
||||||
|
if(cxt.is_recording_deferred_calls) {
|
||||||
|
return has_new_target
|
||||||
|
? new original(...args)
|
||||||
|
: original.apply(this, args)
|
||||||
|
}
|
||||||
|
|
||||||
if(cxt.io_cache_is_recording) {
|
if(cxt.io_cache_is_recording) {
|
||||||
let ok, value, error
|
let ok, value, error
|
||||||
const has_new_target = new.target != null
|
|
||||||
try {
|
try {
|
||||||
// TODO. Do we need it here? Only need for IO calls view. And also
|
// save call, so on expand_call and find_call IO functions would not be
|
||||||
// for expand_call and find_call, to not use cache on expand call
|
// called.
|
||||||
// and find_call
|
// TODO: we have a problem when IO function is called from third-party
|
||||||
|
// lib and async context is lost
|
||||||
set_record_call(cxt)
|
set_record_call(cxt)
|
||||||
|
|
||||||
const index = cxt.io_cache.length
|
const index = cxt.io_cache.length
|
||||||
@@ -57,27 +50,32 @@ const io_patch = (cxt, path, use_context = false) => {
|
|||||||
args = args.slice()
|
args = args.slice()
|
||||||
// Patch callback
|
// Patch callback
|
||||||
const cb = args[0]
|
const cb = args[0]
|
||||||
args[0] = function() {
|
args[0] = Object.defineProperty(function() {
|
||||||
// TODO guard calls from prev runs
|
// TODO if called from prev execution, then throw to
|
||||||
// TODO guard io_cache_is_replay_aborted
|
// finish it ASAP
|
||||||
|
if(cxt.io_cache_is_replay_aborted) {
|
||||||
|
// Non necessary
|
||||||
|
return
|
||||||
|
}
|
||||||
cxt.io_cache.push({type: 'resolution', index})
|
cxt.io_cache.push({type: 'resolution', index})
|
||||||
cb()
|
cb()
|
||||||
}
|
}, 'name', {value: cb.name})
|
||||||
}
|
}
|
||||||
|
|
||||||
value = has_new_target
|
value = has_new_target
|
||||||
? new original(...args)
|
? new original(...args)
|
||||||
: original.apply(this, args)
|
: original.apply(this, args)
|
||||||
|
|
||||||
// TODO remove
|
|
||||||
//console.log('value', value)
|
|
||||||
|
|
||||||
if(value instanceof cxt.window.Promise) {
|
if(value instanceof cxt.window.Promise) {
|
||||||
// TODO use cxt.promise_then, not finally which calls
|
// TODO use cxt.promise_then, not finally which calls
|
||||||
// patched 'then'?
|
// patched 'then'?
|
||||||
value = value.finally(() => {
|
value = value.finally(() => {
|
||||||
// TODO guard calls from prev runs
|
// TODO if called from prev execution, then throw to
|
||||||
// TODO guard io_cache_is_replay_aborted
|
// finish it ASAP
|
||||||
|
if(cxt.io_cache_is_replay_aborted) {
|
||||||
|
// Non necessary
|
||||||
|
return
|
||||||
|
}
|
||||||
cxt.io_cache.push({type: 'resolution', index})
|
cxt.io_cache.push({type: 'resolution', index})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -106,65 +104,47 @@ const io_patch = (cxt, path, use_context = false) => {
|
|||||||
} else {
|
} else {
|
||||||
const call = cxt.io_cache[cxt.io_cache_index]
|
const call = cxt.io_cache[cxt.io_cache_index]
|
||||||
|
|
||||||
/* TODO remove
|
// TODO if call == null or call.type == 'resolution', then do not discard
|
||||||
console.log(
|
// cache, instead switch to record mode and append new calls to the
|
||||||
call.type != 'call'
|
// cache?
|
||||||
, call == null
|
|
||||||
, call.has_new_target != (new.target != null)
|
|
||||||
, call.use_context && (call.context != this)
|
|
||||||
, call.name != name
|
|
||||||
, JSON.stringify(call.args) != JSON.stringify(args)
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO if call.type != 'call', and there are no more calls, should
|
|
||||||
// we abort, or just record one more call?
|
|
||||||
|
|
||||||
if(
|
if(
|
||||||
call == null
|
call == null
|
||||||
|| call.type != 'call'
|
|| call.type != 'call'
|
||||||
|| call.has_new_target != (new.target != null)
|
|| call.has_new_target != has_new_target
|
||||||
// TODO test
|
|
||||||
|| call.use_context && (call.context != this)
|
|| call.use_context && (call.context != this)
|
||||||
|| call.name != name
|
|| call.name != name
|
||||||
|| (
|
|| (
|
||||||
// TODO for setTimeout, compare last arg (timeout)
|
(name == 'setTimeout' && (args[1] != call.args[1])) /* compares timeout*/
|
||||||
name != 'setTimeout'
|
||
|
||||||
&&
|
(
|
||||||
JSON.stringify(call.args) != JSON.stringify(args)
|
name != 'setTimeout'
|
||||||
|
&&
|
||||||
|
JSON.stringify(call.args) != JSON.stringify(args)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
){
|
){
|
||||||
//TODO remove console.error('DISCARD cache', call)
|
|
||||||
cxt.io_cache_is_replay_aborted = true
|
cxt.io_cache_is_replay_aborted = true
|
||||||
// Try to finish fast
|
// Try to finish fast
|
||||||
throw new Error('io replay aborted')
|
throw new Error('io replay aborted')
|
||||||
} else {
|
} else {
|
||||||
// TODO remove console.log('cached call found', call)
|
|
||||||
const next_resolution = cxt.io_cache.find((e, i) =>
|
const next_resolution = cxt.io_cache.find((e, i) =>
|
||||||
e.type == 'resolution' && i > cxt.io_cache_index
|
e.type == 'resolution' && i > cxt.io_cache_index
|
||||||
)
|
)
|
||||||
|
|
||||||
if(next_resolution != null && !cxt.io_cache_resolver_is_set) {
|
if(next_resolution != null && !cxt.io_cache_resolver_is_set) {
|
||||||
console.error('set resolver')
|
|
||||||
const original_setTimeout = cxt.window.setTimeout.__original
|
const original_setTimeout = cxt.window.setTimeout.__original
|
||||||
cxt.io_cache_resolver_is_set = true
|
cxt.io_cache_resolver_is_set = true
|
||||||
|
|
||||||
original_setTimeout(() => {
|
original_setTimeout(() => {
|
||||||
|
// TODO if called from prev execution, then throw to finish it ASAP
|
||||||
|
|
||||||
if(cxt.io_cache_is_replay_aborted) {
|
if(cxt.io_cache_is_replay_aborted) {
|
||||||
console.error('RESOLVER ABORTED')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO guard from previous run
|
|
||||||
console.error('resolver', {
|
|
||||||
io_cache_is_replay_aborted: cxt.io_cache_is_replay_aborted,
|
|
||||||
io_cache_index: cxt.io_cache_index,
|
|
||||||
})
|
|
||||||
|
|
||||||
cxt.io_cache_resolver_is_set = false
|
cxt.io_cache_resolver_is_set = false
|
||||||
|
|
||||||
// TODO check if call from prev run
|
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if(cxt.io_cache_index >= cxt.io_cache.length) {
|
if(cxt.io_cache_index >= cxt.io_cache.length) {
|
||||||
throw new Error('illegal state')
|
throw new Error('illegal state')
|
||||||
@@ -172,7 +152,6 @@ const io_patch = (cxt, path, use_context = false) => {
|
|||||||
|
|
||||||
const next_event = cxt.io_cache[cxt.io_cache_index]
|
const next_event = cxt.io_cache[cxt.io_cache_index]
|
||||||
if(next_event.type == 'call') {
|
if(next_event.type == 'call') {
|
||||||
// TODO Call not happened, replay?
|
|
||||||
cxt.io_cache_is_replay_aborted = true
|
cxt.io_cache_is_replay_aborted = true
|
||||||
} else {
|
} else {
|
||||||
while(
|
while(
|
||||||
@@ -190,7 +169,6 @@ const io_patch = (cxt, path, use_context = false) => {
|
|||||||
} else {
|
} else {
|
||||||
resolver(cxt.io_cache[resolution.index].value)
|
resolver(cxt.io_cache[resolution.index].value)
|
||||||
}
|
}
|
||||||
// TODO remove console.log('RESOLVE', cxt.io_cache_index, resolution.index)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,15 +221,20 @@ const Response_methods = [
|
|||||||
'text',
|
'text',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// TODO bare IO functions should not be exposed at all, to allow calling it
|
||||||
|
// only from patched versions. Especially setInterval which can cause leaks
|
||||||
export const apply_io_patches = cxt => {
|
export const apply_io_patches = cxt => {
|
||||||
io_patch(cxt, ['Math', 'random'])
|
io_patch(cxt, ['Math', 'random'])
|
||||||
|
|
||||||
io_patch(cxt, ['setTimeout'])
|
io_patch(cxt, ['setTimeout'])
|
||||||
// TODO test
|
// TODO if call setTimeout and then clearTimeout, cache it and remove call of
|
||||||
|
// clearTimeout, and make only setTimeout, then it would never be called when
|
||||||
|
// replaying from cache
|
||||||
io_patch(cxt, ['clearTimeout'])
|
io_patch(cxt, ['clearTimeout'])
|
||||||
|
|
||||||
|
|
||||||
// TODO test
|
// TODO patch setInterval to only cleanup all intervals on finish
|
||||||
|
|
||||||
const Date = cxt.window.Date
|
const Date = cxt.window.Date
|
||||||
io_patch(cxt, ['Date'])
|
io_patch(cxt, ['Date'])
|
||||||
cxt.window.Date.parse = Date.parse
|
cxt.window.Date.parse = Date.parse
|
||||||
@@ -273,7 +256,6 @@ export const remove_io_patches = cxt => {
|
|||||||
io_patch_remove(cxt, ['Math', 'random'])
|
io_patch_remove(cxt, ['Math', 'random'])
|
||||||
|
|
||||||
io_patch_remove(cxt, ['setTimeout'])
|
io_patch_remove(cxt, ['setTimeout'])
|
||||||
// TODO test
|
|
||||||
io_patch_remove(cxt, ['clearTimeout'])
|
io_patch_remove(cxt, ['clearTimeout'])
|
||||||
|
|
||||||
io_patch_remove(cxt, ['Date'])
|
io_patch_remove(cxt, ['Date'])
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ const gen_to_promise = gen_fn => {
|
|||||||
const do_run = function*(module_fns, cxt, io_cache){
|
const do_run = function*(module_fns, cxt, io_cache){
|
||||||
let calltree
|
let calltree
|
||||||
|
|
||||||
cxt = io_cache == null
|
cxt = (io_cache == null || io_cache.length == 0)
|
||||||
// TODO move all io_cache properties to the object?
|
// TODO group all io_cache_ properties to single object?
|
||||||
? {...cxt,
|
? {...cxt,
|
||||||
io_cache_is_recording: true,
|
io_cache_is_recording: true,
|
||||||
io_cache: [],
|
io_cache: [],
|
||||||
|
|||||||
24
test/test.js
24
test/test.js
@@ -3050,15 +3050,18 @@ const y = x()`
|
|||||||
|
|
||||||
const next = COMMANDS.input(initial, `const x = Math.random()*2`, 0).state
|
const next = COMMANDS.input(initial, `const x = Math.random()*2`, 0).state
|
||||||
assert_equal(next.value_explorer.result.value, 2)
|
assert_equal(next.value_explorer.result.value, 2)
|
||||||
|
assert_equal(next.eval_cxt.io_cache_index, 1)
|
||||||
|
|
||||||
// Patch Math.random to return 2. Now the first call to Math.random() is
|
// Patch Math.random to return 2.
|
||||||
// cached with value 1, and the second shoud return 2
|
// TODO The first call to Math.random() is cached with value 1, and the
|
||||||
|
// second shoud return 2
|
||||||
Object.assign(globalThis.run_window.Math, {random: () => 2})
|
Object.assign(globalThis.run_window.Math, {random: () => 2})
|
||||||
const replay_failed = COMMANDS.input(
|
const replay_failed = COMMANDS.input(
|
||||||
initial,
|
initial,
|
||||||
`const x = Math.random() + Math.random()`,
|
`const x = Math.random() + Math.random()`,
|
||||||
0
|
0
|
||||||
).state
|
).state
|
||||||
|
|
||||||
// TODO must reuse first cached call?
|
// TODO must reuse first cached call?
|
||||||
assert_equal(replay_failed.value_explorer.result.value, 4)
|
assert_equal(replay_failed.value_explorer.result.value, 4)
|
||||||
|
|
||||||
@@ -3211,4 +3214,21 @@ const y = x()`
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
test('record io no io cache on deferred calls', async () => {
|
||||||
|
const code = `
|
||||||
|
const x = Math.random
|
||||||
|
export const fn = () => x()
|
||||||
|
`
|
||||||
|
|
||||||
|
const {state: i, on_deferred_call} = test_deferred_calls_state(code)
|
||||||
|
|
||||||
|
// Make deferred call
|
||||||
|
i.modules[''].fn()
|
||||||
|
|
||||||
|
const state = on_deferred_call(i)
|
||||||
|
|
||||||
|
// Deferred calls should not be record in cache
|
||||||
|
assert_equal(state.eval_cxt.io_cache.length, 0)
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user