Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

自定义backend遇到的一些困惑 #198

Open
fanyipin opened this issue Sep 20, 2024 · 10 comments
Open

自定义backend遇到的一些困惑 #198

fanyipin opened this issue Sep 20, 2024 · 10 comments

Comments

@fanyipin
Copy link

fanyipin commented Sep 20, 2024

我想做的事情大概是这样:在electron中自定义backend,模式为shadowroot,其中两个webview,service webview运行glass-easel,view webview通过接收service webview的消息进行对dom节点进行绘制,两个webview间通过ipc通信。 在实施过程中,发现如果有1000个循环节点的发,要发送14000多次消息,渲染延迟1-2s。我理解是不是某些service的动作不需要通信消耗,这块有什么好建议吗?感觉现在通信消耗大大的延缓了界面的绘制与响应。
或者这个方向可行吗

@fanyipin
Copy link
Author

我设想的是service层在backend的方法例如 appendchild,replacechild等方法中发送消息到view层,view层利用原生的appendchild等方式实现

@fanyipin fanyipin changed the title 自定义backend 自定义backend遇到的一些困惑 Sep 20, 2024
@Tidyzq
Copy link
Contributor

Tidyzq commented Sep 20, 2024

这个方向是可行的,但是不能直接将 shadow 协议的后端操作对应到原生方法上。
可以参考这个实现 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/src/backend.ts

@LastLeaf
Copy link
Member

我没尝试过这么做,不过根据经验可以有两个猜测。

  • 通信本身的延迟太大:这个需要你优化一下通信协议,特别是要注意下是不是需要做一些“消息合并”之类的。
  • 界面更新开销太大:最终对 DOM 树的变更应该尽可能在更少的 js task 中完成,换句话说,一次 setData 调用可能会产生很多 backend 调用,这些 backend 调用应当在同一个 js task 中调用到 DOM 。

@fanyipin
Copy link
Author

这个方向是可行的,但是不能直接将 shadow 协议的后端操作对应到原生方法上。 可以参考这个实现 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/src/backend.ts

嗯,好的,我先学习一下

@fanyipin
Copy link
Author

我没尝试过这么做,不过根据经验可以有两个猜测。

  • 通信本身的延迟太大:这个需要你优化一下通信协议,特别是要注意下是不是需要做一些“消息合并”之类的。
  • 界面更新开销太大:最终对 DOM 树的变更应该尽可能在更少的 js task 中完成,换句话说,一次 setData 调用可能会产生很多 backend 调用,这些 backend 调用应当在同一个 js task 中调用到 DOM 。

嗯,消息合并之类的操作已经做过了。后面的dom树变更优化我再想下

还有我看glass-easel要在10月1号完成 MiniProgram 环境下的 webview 后端,这个任务是指将service和view拆分到两个webview去渲染吗?是跟我上面提的事儿类似吗

@fanyipin
Copy link
Author

这个方向是可行的,但是不能直接将 shadow 协议的后端操作对应到原生方法上。 可以参考这个实现 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/src/backend.ts

@Tidyzq 抱歉,没有完全看懂这段代码,和我理解的有点儿不太一样,有几个小疑惑希望能帮忙解答一下:

  1. 目前看来也是将backend的每一个动作都要进行通信吗?只不过是每一个动作并不对应一个document的原生动作?
  2. view_controller的作用不是特别明白,可以展开讲讲不?
  3. 这个backend的实现有验证过性能吗?我简单的试了下,如果是循环生成300个列表(列表为文本),在service即backend动作发送消息的时间间隔就已经达到3-4百毫秒级了(这里只统计一次渲染的消息发送间隔),这样的话性能不会受影响吗?当然也有可能是我用法不对,可以帮忙提供一个简单的使用示例吗?

@LastLeaf
Copy link
Member

我没尝试过这么做,不过根据经验可以有两个猜测。

  • 通信本身的延迟太大:这个需要你优化一下通信协议,特别是要注意下是不是需要做一些“消息合并”之类的。
  • 界面更新开销太大:最终对 DOM 树的变更应该尽可能在更少的 js task 中完成,换句话说,一次 setData 调用可能会产生很多 backend 调用,这些 backend 调用应当在同一个 js task 中调用到 DOM 。

嗯,消息合并之类的操作已经做过了。后面的dom树变更优化我再想下

还有我看glass-easel要在10月1号完成 MiniProgram 环境下的 webview 后端,这个任务是指将service和view拆分到两个webview去渲染吗?是跟我上面提的事儿类似吗

milestone 的 deadline 会根据实际情况调整,并不准确。 glass-easel 的 milestone 通常是项目自身的目标,和正式 landing 到 MiniProgram 环境也不是一回事。 milestone v1.0 的主要目标是接口全面稳定。

@fanyipin
Copy link
Author

fanyipin commented Oct 8, 2024

@Tidyzq 我直接引用了https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/src/backend.ts里面的代码,并把channel替换成了electron的ipc通信,发现还是存在上面的问题:
我的小程序代码大概如下:
js:

Page({
  data: {
    showAgain: false,
    num: 1,
    list: ['tesxt', 'fjeijfioef', 'fejfiejofjeojfoie'],
    renderList:  ["1", "2"]
  },
  helloTap() {
    this.setData({
      showAgain: !this.data.showAgain,
      num: new Date().getTime(),
      renderList: []
    })
    this.setData({
      showAgain: !this.data.showAgain,
      num: new Date().getTime(),
      renderList: Array.from({length: 300}, (_, i) => i + parseFloat(Math.random().toString()).toFixed(2))
    })
  },
})

wxml:

<view wx:for="{{renderList}}">hello-{{index}}-{{item}}</view>
<view class="hello" bind:tap="helloTap">
  Hello world! {{num}}
</view>
<wx-progress></wx-progress>

在点击的时候响应很慢,这个消耗貌似不在通信,我看service从接收到点击事件到最后一次的时间间隔都有将近2s的时间,具体可见下图:

github-backend.bak.mp4

上图中右侧为service即glass-easel backend运行的webview,在点击时,我会先清空数组,然后再赋值300项,并动态渲染元素,可以看到在backend侧间隔时间就要接近2s,所以是不是我的用法有问题?

我本意是想要用glass-easel用来模拟小程序的整体架构,在electron中,backend运行在service的webview中,另一个view的webview接收service传递的消息,现在发现渲染性能并不能满足需求,是我的打开方式有问题吗?

我理解通过glass-easel可以将逻辑处理和渲染层renderer分开,这样我就可以在service中处理逻辑,view中接收service的消息进行渲染,可目前看效果不能达到预期,我这样的设计思路和glass-easel的架构思路相符吗? @LastLeaf @Tidyzq 有时间还请指导一下

@Tidyzq
Copy link
Contributor

Tidyzq commented Oct 8, 2024

  1. 目前看来也是将backend的每一个动作都要进行通信吗?只不过是每一个动作并不对应一个document的原生动作?
  2. view_controller的作用不是特别明白,可以展开讲讲不?
  3. 这个backend的实现有验证过性能吗?我简单的试了下,如果是循环生成300个列表(列表为文本),在service即backend动作发送消息的时间间隔就已经达到3-4百毫秒级了(这里只统计一次渲染的消息发送间隔),这样的话性能不会受影响吗?当然也有可能是我用法不对,可以帮忙提供一个简单的使用示例吗?

@fanyipin

  1. 通信应该要做合并操作,而不是每个动作都进行一次通讯。简单的你可以每个微任务合并成一个,复杂的应该是根据 setData 以及事件触发做合并。
  2. backend.ts 用于逻辑侧,view_controller.ts 用于渲染侧。这是一个还未完成的模块所以没有完善的指引,大致使用方式是:
    • 在逻辑侧使用 backend.ts 提供的 ShadowDomBackendContext 作为自定义渲染后端。
    • 在逻辑侧使用 MessageChannelDataSide 将后端操作转换为指令流,传入具体的通讯实现。(你应该在这里做合并)
    • 渲染侧需要准备好 glass_easel 环境,一个渲染后端,以及一个挂载点。
    • 在渲染侧使用 view_controller.ts 提供的 MessageChannelViewSide 接收指令流,传入具体的通讯实现。
    • 在渲染侧使用 ViewController 将指令流转换为挂载点上的树操作。
      简单用法可以参考模块内的单元测试代码 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/tests/base/env.ts。
  3. 用单元测试代码即可验证,验证过没有你说的这么大开销。

在点击的时候响应很慢,这个消耗貌似不在通信,我看service从接收到点击事件到最后一次的时间间隔都有将近2s的时间,具体可见下图:

github-backend.bak.mp4
上图中右侧为service即glass-easel backend运行的webview,在点击时,我会先清空数组,然后再赋值300项,并动态渲染元素,可以看到在backend侧间隔时间就要接近2s,所以是不是我的用法有问题?

从视频上看你没有做通讯合并操作,耗时花费在了单次操作的通讯消耗。你可以尝试每个微任务合并通讯。

  // 参考 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/tests/base/env.ts。
  const createBridge = () => {
    let _cb: ((...args: any[]) => void) | null = null
    const subscribe = (cb: (...args: any[]) => void) => {
      _cb = cb
    }
    let queue = []
    const publish = (args: readonly any[]): void => {
      // 每个微任务合并
      queue.push(args)
      if (queue.length === 1) {
        Promise.resolve().then(() => {
          queue.forEach(args => _cb?.(args))
          queue = []
        })
      }
    }
    return { subscribe, publish }
  }

  const bridgeToView = createBridge()
  const bridgeToData = createBridge()

  const messageChannelDataSide = MessageChannelDataSide(
    bridgeToView.publish,
    bridgeToData.subscribe,
    getLinearIdGenerator,
  )
  const messageChannelViewSide = MessageChannelViewSide(
    bridgeToData.publish,
    bridgeToView.subscribe,
    syncController,
    getLinearIdGenerator,
  )

@fanyipin
Copy link
Author

fanyipin commented Oct 8, 2024

  1. 目前看来也是将backend的每一个动作都要进行通信吗?只不过是每一个动作并不对应一个document的原生动作?
  2. view_controller的作用不是特别明白,可以展开讲讲不?
  3. 这个backend的实现有验证过性能吗?我简单的试了下,如果是循环生成300个列表(列表为文本),在service即backend动作发送消息的时间间隔就已经达到3-4百毫秒级了(这里只统计一次渲染的消息发送间隔),这样的话性能不会受影响吗?当然也有可能是我用法不对,可以帮忙提供一个简单的使用示例吗?

@fanyipin

  1. 通信应该要做合并操作,而不是每个动作都进行一次通讯。简单的你可以每个微任务合并成一个,复杂的应该是根据 setData 以及事件触发做合并。

  2. backend.ts 用于逻辑侧,view_controller.ts 用于渲染侧。这是一个还未完成的模块所以没有完善的指引,大致使用方式是:

    • 在逻辑侧使用 backend.ts 提供的 ShadowDomBackendContext 作为自定义渲染后端。
    • 在逻辑侧使用 MessageChannelDataSide 将后端操作转换为指令流,传入具体的通讯实现。(你应该在这里做合并)
    • 渲染侧需要准备好 glass_easel 环境,一个渲染后端,以及一个挂载点。
    • 在渲染侧使用 view_controller.ts 提供的 MessageChannelViewSide 接收指令流,传入具体的通讯实现。
    • 在渲染侧使用 ViewController 将指令流转换为挂载点上的树操作。
      简单用法可以参考模块内的单元测试代码 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/tests/base/env.ts。
  3. 用单元测试代码即可验证,验证过没有你说的这么大开销。

在点击的时候响应很慢,这个消耗貌似不在通信,我看service从接收到点击事件到最后一次的时间间隔都有将近2s的时间,具体可见下图:
github-backend.bak.mp4
上图中右侧为service即glass-easel backend运行的webview,在点击时,我会先清空数组,然后再赋值300项,并动态渲染元素,可以看到在backend侧间隔时间就要接近2s,所以是不是我的用法有问题?

从视频上看你没有做通讯合并操作,耗时花费在了单次操作的通讯消耗。你可以尝试每个微任务合并通讯。

  // 参考 https://github.com/Tidyzq/glass-easel/blob/wip-shadow-sync/glass-easel-shadow-sync/tests/base/env.ts。
  const createBridge = () => {
    let _cb: ((...args: any[]) => void) | null = null
    const subscribe = (cb: (...args: any[]) => void) => {
      _cb = cb
    }
    let queue = []
    const publish = (args: readonly any[]): void => {
      // 每个微任务合并
      queue.push(args)
      if (queue.length === 1) {
        Promise.resolve().then(() => {
          queue.forEach(args => _cb?.(args))
          queue = []
        })
      }
    }
    return { subscribe, publish }
  }

  const bridgeToView = createBridge()
  const bridgeToData = createBridge()

  const messageChannelDataSide = MessageChannelDataSide(
    bridgeToView.publish,
    bridgeToData.subscribe,
    getLinearIdGenerator,
  )
  const messageChannelViewSide = MessageChannelViewSide(
    bridgeToData.publish,
    bridgeToView.subscribe,
    syncController,
    getLinearIdGenerator,
  )

嗯,我再尝试下,这个应该也跟我开着devtool + console有关。不过有一点还是不太了解,用户点击事件到service接收响应再到setData目前不都是框架本身处理的吗?我怎么根据setData去做合并呢?我现在了解到的还是在自定义的context中的相关api里实现自定义逻辑

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants