record io

This commit is contained in:
Dmitry Vasilev
2023-02-06 01:53:34 +08:00
parent 4410d4135a
commit be4d104dc6
16 changed files with 25216 additions and 35 deletions

261
record_io/ethers.html Normal file
View 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

File diff suppressed because one or more lines are too long

28
record_io/fetch.html Normal file
View 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>

View 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
View 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
View 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
View 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