mirror of
https://github.com/leporello-js/leporello-js
synced 2026-01-13 21:14:28 -08:00
record io
This commit is contained in:
261
record_io/ethers.html
Normal file
261
record_io/ethers.html
Normal file
@@ -0,0 +1,261 @@
|
||||
<script>
|
||||
let io_cache = null
|
||||
|
||||
let io_cache_is_recording = io_cache == null
|
||||
if(io_cache == null) {
|
||||
io_cache = {calls: [], resolution_order: []}
|
||||
}
|
||||
let io_cache_is_replay_aborted = false
|
||||
let io_cache_index = 0
|
||||
|
||||
const io_patch = (obj, method, name, use_context = false) => {
|
||||
if(obj == null || obj[method] == null) {
|
||||
// Method is absent in current env, skip patching
|
||||
return
|
||||
}
|
||||
const original = obj[method]
|
||||
obj[method] = function(...args) {
|
||||
console.log('patched method', name, {io_cache_is_replay_aborted, io_cache_is_recording})
|
||||
// TODO guard that in find_call io methods are not called?
|
||||
// if(searched_location != null) {
|
||||
// throw new Error('illegal state')
|
||||
// }
|
||||
if(io_cache_is_replay_aborted) {
|
||||
// Try to finish fast
|
||||
console.error('ABORT')
|
||||
throw new Error('io recording aborted')
|
||||
} else if(io_cache_is_recording) {
|
||||
let ok, value, error
|
||||
const has_new_target = new.target != null
|
||||
try {
|
||||
// TODO. Do we need it here? Only need for IO calls view. And also
|
||||
// for expand_call and find_call, to not use cache on expand call
|
||||
// and find_call
|
||||
//set_record_call()
|
||||
value = has_new_target
|
||||
? new original(...args)
|
||||
: original.apply(this, args)
|
||||
|
||||
console.log('value', value)
|
||||
|
||||
//const index = io_cache.calls.length
|
||||
//if(value instanceof Promise) {
|
||||
// value.finally(() => {
|
||||
// console.log('resolved', index)
|
||||
// io_cache.resolution_order.push(index)
|
||||
// })
|
||||
//} else {
|
||||
// io_cache.resolution_order.push(index)
|
||||
//}
|
||||
|
||||
/* TODO remove
|
||||
if(value instanceof Promise) {
|
||||
const original_value = value
|
||||
value = new Promise((resolve, reject) => {
|
||||
// TODO fix setTimeout.original
|
||||
globalThis.setTimeout.original(
|
||||
() => {
|
||||
original_value.then(resolve, reject)
|
||||
},
|
||||
10
|
||||
)
|
||||
})
|
||||
}
|
||||
*/
|
||||
// TODO
|
||||
//if(value instanceof Promise) {
|
||||
// const make_cb = ok => value => {
|
||||
// // TODO resolve promises in the same order they were resolved on
|
||||
// initial execution
|
||||
|
||||
// }
|
||||
// // TODO should we use promise_then or patched promise.then?
|
||||
// promise_then.apply(value, make_cb(true), make_cb(false))
|
||||
//}
|
||||
ok = true
|
||||
return value
|
||||
} catch(e) {
|
||||
error = e
|
||||
ok = false
|
||||
throw e
|
||||
} finally {
|
||||
io_cache.calls.push({
|
||||
ok,
|
||||
value,
|
||||
error,
|
||||
args,
|
||||
name,
|
||||
// To discern calls with and without 'new' keyword, primary for
|
||||
// Date that can be called with and without new
|
||||
has_new_target,
|
||||
use_context,
|
||||
context: use_context ? this : undefined,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const call = io_cache.calls[io_cache_index++]
|
||||
/*
|
||||
TODO remove
|
||||
console.log(
|
||||
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)
|
||||
)
|
||||
*/
|
||||
if(
|
||||
call == null
|
||||
|| call.has_new_target != (new.target != null)
|
||||
// TODO test
|
||||
|| call.use_context && (call.context != this)
|
||||
|| call.name != name
|
||||
|| JSON.stringify(call.args) != JSON.stringify(args)
|
||||
){
|
||||
console.log('discard cache', call)
|
||||
io_cache_is_replay_aborted = true
|
||||
// Try to finish fast
|
||||
throw new Error('io replay aborted')
|
||||
} else {
|
||||
console.log('cached call found', call)
|
||||
if(call.ok) {
|
||||
// TODO resolve promises in the same order they were resolved on
|
||||
// initial execution
|
||||
|
||||
if(call.value instanceof Promise) {
|
||||
const original_setTimeout = globalThis.setTimeout.original
|
||||
return Promise.all([
|
||||
call.value,
|
||||
new Promise(resolve => original_setTimeout(
|
||||
resolve,
|
||||
10
|
||||
))
|
||||
]).then(([a,_]) => a)
|
||||
// TODO remove
|
||||
.then(x => {console.log('resolved',name); return x})
|
||||
} else {
|
||||
return call.value
|
||||
}
|
||||
} else {
|
||||
throw call.error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(obj[method], 'name', {value: original.name})
|
||||
|
||||
obj[method].__original = original
|
||||
}
|
||||
|
||||
function io_patch_remove(obj, method) {
|
||||
if(obj == null || obj[method] == null) {
|
||||
// Method is absent in current env, skip patching
|
||||
return
|
||||
}
|
||||
obj[method] = obj[method].__original
|
||||
}
|
||||
|
||||
const Response_methods = [
|
||||
'arrayBuffer',
|
||||
'blob',
|
||||
'formData',
|
||||
'json',
|
||||
'text',
|
||||
]
|
||||
|
||||
function apply_io_patches() {
|
||||
io_patch(Math, 'random', 'Math.random')
|
||||
|
||||
|
||||
// TODO test
|
||||
const Date = globalThis.Date
|
||||
io_patch(globalThis, 'Date', 'Date')
|
||||
globalThis.Date.parse = Date.parse
|
||||
globalThis.Date.now = Date.now
|
||||
globalThis.Date.UTC = Date.UTC
|
||||
globalThis.Date.length = Date.length
|
||||
globalThis.Date.name = Date.name
|
||||
io_patch(globalThis.Date, 'now', 'Date.now')
|
||||
|
||||
|
||||
io_patch(globalThis, 'fetch', 'fetch')
|
||||
// Check if Response is defined, for node.js
|
||||
if(globalThis.Response != null) {
|
||||
for(let key of Response_methods) {
|
||||
io_patch(Response.prototype, key, 'Response.prototype.' + key, true)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO
|
||||
const setTimeout = globalThis.setTimeout
|
||||
globalThis.setTimeout = function(cb, timeout) {
|
||||
const timer_id = setTimeout(function(...args) {
|
||||
console.log('timeout', timer_id)
|
||||
cb(...args)
|
||||
}, timeout)
|
||||
console.log('setTimeout', timer_id)
|
||||
return timer_id
|
||||
}
|
||||
globalThis.setTimeout.original = setTimeout
|
||||
|
||||
|
||||
// TODO clearTimeout
|
||||
}
|
||||
|
||||
function remove_io_patches() {
|
||||
// TODO when to apply io_patches and promise_patches? Only once, when we
|
||||
// create window?
|
||||
|
||||
io_patch_remove(Math, 'random')
|
||||
io_patch_remove(globalThis, 'Date')
|
||||
io_patch_remove(globalThis, 'fetch')
|
||||
|
||||
// Check if Response is defined, for node.js
|
||||
if(globalThis.Response != null) {
|
||||
for(let key of Response_methods) {
|
||||
io_patch_remove(Response.prototype, key)
|
||||
}
|
||||
}
|
||||
globalThis.setTimeout = globalThis.setTimeout.original
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type='module'>
|
||||
|
||||
|
||||
//import {ethers} from 'https://unpkg.com/ethers/dist/ethers.esm.js'
|
||||
import {ethers} from './ethers.js'
|
||||
|
||||
async function run() {
|
||||
|
||||
const URL = 'https://ethereum-goerli-rpc.allthatnode.com'
|
||||
|
||||
const p = ethers.getDefaultProvider(URL)
|
||||
|
||||
const latest = await p.getBlock()
|
||||
|
||||
const txs = await Promise.all(latest.transactions.slice(0,1).map(async (t, i) => {
|
||||
console.error("GETTING RECEIPT", i)
|
||||
const result = await p.getTransactionReceipt(t)
|
||||
console.error("GOT RECEIPT", i)
|
||||
return result
|
||||
}))
|
||||
|
||||
const totalGas = txs.reduce((gas,tx) =>
|
||||
gas.add(tx.gasUsed), ethers.BigNumber.from(0))
|
||||
|
||||
console.log('GAS', totalGas.add(3))
|
||||
}
|
||||
|
||||
|
||||
apply_io_patches()
|
||||
|
||||
await run()
|
||||
|
||||
io_cache_is_recording = false
|
||||
console.error('REPLAY')
|
||||
await run()
|
||||
|
||||
|
||||
</script>
|
||||
24280
record_io/ethers.js
Normal file
24280
record_io/ethers.js
Normal file
File diff suppressed because one or more lines are too long
28
record_io/fetch.html
Normal file
28
record_io/fetch.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<script type='module'>
|
||||
|
||||
const original = globalThis.fetch
|
||||
globalThis.fetch = function(...args) {
|
||||
console.log('fetch called')
|
||||
return original.apply(null, args)
|
||||
}
|
||||
|
||||
for(let key of [
|
||||
'arrayBuffer',
|
||||
'blob',
|
||||
'formData',
|
||||
'json',
|
||||
'text',
|
||||
]) {
|
||||
|
||||
let original = Response.prototype[key]
|
||||
Response.prototype[key] = function(...args){
|
||||
console.log('key called', key)
|
||||
return original.apply(this, args)
|
||||
}
|
||||
}
|
||||
|
||||
console.log((await (await fetch('/')).text()).length)
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
26
record_io/microtask_test.js
Normal file
26
record_io/microtask_test.js
Normal file
@@ -0,0 +1,26 @@
|
||||
console.log('start')
|
||||
|
||||
let r
|
||||
const x = new Promise(resolve => r = resolve).then(() => {console.log('resolved')})
|
||||
|
||||
console.log('before resolve')
|
||||
r()
|
||||
console.log('after resolve')
|
||||
/*
|
||||
console.log('start')
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
console.log('1')
|
||||
Promise.resolve().then(() => {
|
||||
console.log('2')
|
||||
})
|
||||
})
|
||||
|
||||
console.log('end')
|
||||
Promise.resolve().then(() => {
|
||||
console.log('3')
|
||||
Promise.resolve().then(() => {
|
||||
console.log('4')
|
||||
})
|
||||
})
|
||||
*/
|
||||
16
record_io/new.js
Normal file
16
record_io/new.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
function f() {
|
||||
console.log('n', new.target)
|
||||
}
|
||||
|
||||
f()
|
||||
new f()
|
||||
*/
|
||||
|
||||
const f = new Function(`
|
||||
return arguments.length
|
||||
`)
|
||||
|
||||
|
||||
console.log(f(1,2,3))
|
||||
console.log(f(1,2,3,4))
|
||||
33
record_io/promise.js
Normal file
33
record_io/promise.js
Normal file
@@ -0,0 +1,33 @@
|
||||
//let value = Promise.reject(1)
|
||||
|
||||
/*
|
||||
value.then(
|
||||
() => console.log('res'),
|
||||
() => console.log('rej'),
|
||||
)
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
const original_value = value
|
||||
|
||||
value = new Promise((resolve, reject) => {
|
||||
globalThis.setTimeout(
|
||||
() => {
|
||||
console.log('timeout')
|
||||
original_value.then(resolve, reject)
|
||||
},
|
||||
1000
|
||||
)
|
||||
})
|
||||
|
||||
try {
|
||||
console.log(await value)
|
||||
} catch(e) {
|
||||
console.log('ERROR', e)
|
||||
}
|
||||
*/
|
||||
|
||||
const t = globalThis.setTimeout
|
||||
|
||||
t(() => console.log('timeout'), 100)
|
||||
73
record_io/timelime
Normal file
73
record_io/timelime
Normal file
@@ -0,0 +1,73 @@
|
||||
Timeline Replay
|
||||
|
||||
call a() call a()
|
||||
resolve a()
|
||||
|
||||
call b()
|
||||
resolve b()
|
||||
|
||||
call c()
|
||||
resolve c()
|
||||
|
||||
|
||||
Timeline Replay
|
||||
|
||||
resolution_index = 0, io_index = 0
|
||||
|
||||
call a() call a: return promise
|
||||
compare resolutions[resolution_index] with io_index
|
||||
io_index < resolutions[0]
|
||||
do not resolve
|
||||
io_index++
|
||||
|
||||
call b() call b: return promise
|
||||
compare resolutions[0] && io_index
|
||||
io_index < resolutions[0]
|
||||
do not resolve
|
||||
|
||||
call c() call c: return promise
|
||||
|
||||
resolve c()
|
||||
resolve b()
|
||||
resolve a()
|
||||
|
||||
|
||||
resolutions: [
|
||||
3,
|
||||
2,
|
||||
1,
|
||||
]
|
||||
|
||||
Делаем реплей. К нам приходят события - вызовы функций. Мы перехватываем вызов, возвращаем промис, и ресолвим тот промис, который сейчас надо заресолвить. Например, в примере выше мы ресолвим a() после вызова с(). А b() ресолвим после ресолва с(). То есть мы можем ресолвить несколько за раз.
|
||||
|
||||
|
||||
|
||||
Record: [
|
||||
call a
|
||||
resolve a
|
||||
call b
|
||||
resolve b
|
||||
]
|
||||
|
||||
Replay: [
|
||||
|
||||
call a
|
||||
смотрим что возвращается промис, взводим ресолвер
|
||||
|
||||
ресолвер сработал
|
||||
resolve a
|
||||
|
||||
call b
|
||||
смотрим что возвращается промис, взводим ресолвер
|
||||
|
||||
ресолвер сработал
|
||||
resolve b
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
call
|
||||
resolve
|
||||
Reference in New Issue
Block a user