NVIDIA Omniverse USD插件开发中的UI卡顿问题复现及分析

描述

UI 卡顿问题复现及分析

在进行 NVIDIA Omniverse USD 插件开发的时候遇到了一个性能卡顿的问题,这个功能的初衷是通过路径追踪和原始的绿幕视频,借助 Omniverse Farm 来实现高质量的后期自动化流程,实现思路是记录相机定位 FreeD 的运动轨迹,并记录保存到一个 USD 的 sublayer 当中,根据时间码(Timecode)进行后期自动化合成的流程,流程图如下:

摄影机

在外部摄影机记录原始的绿幕影片素材的时候,点击开始 / 结束分别会触发一个时间码 (Timecode)的信号,信号可以在 BMD 采集卡当中通过 SDK 获得,这样我们把从开始到结束的相机定位轨迹记录存至 buffer 中,然后更新到 USD 的 stage sublayer 中。

首先通过 Python API 创建一个 Sublayer,把记录的 sequence 通过 USD time sampler 记录到相机 prim 的 attribute 下面,仅对一万个 time sampler 进行记录并统一写入该 sublayer,后观察到该写入过程耗时达数十秒,且造成 Omniverse 主线程 UI 出现卡顿。经测试,无论采用同步、异步 AsyncIO 或线程方式执行操作,均未使情况得到改善,UI 卡死现象始终存在。(可以查看代码文件中注释的 1、2、3)

Tracy.py 的源代码如下:

 

import omni.kit.app
import time
import asyncio
from pxr import Sdf, Usd, UsdGeom, Gf
from omni.kit.usd.layers import LayerUtils, get_layers, LayerEditMode
import omni.kit.commands
from collections import deque
import os
from typing import List, Tuple
from omni.kit.widget.layers.path_utils import PathUtils
import carb
from typing import List, Tuple
from enum import Enum
import asyncio
from pxr import Sdf, Usd, UsdGeom
from pxr import Usd, UsdGeom, Gf
from omni.kit.async_engine import run_coroutine
from concurrent.futures import ALL_COMPLETED, ThreadPoolExecutor, wait


def create_prim(stage, prim_path="/World/Camera"):
    prim = stage.GetPrimAtPath(prim_path)
    if prim and prim.GetTypeName() == "Camera":
        carb.log_info(f"Camera already exists at: {prim_path}")
        return prim
    else:
        camera_prim = UsdGeom.Camera.Define(stage, prim_path)
        camera_prim.AddTranslateOp().Set(Gf.Vec3d(10, 20, 30)) 
        camera_prim.AddRotateXYZOp().Set(Gf.Vec3f(0, 45, 0)) 
        carb.log_info(f"Created new Camera at: {prim_path}")
        return camera_prim.GetPrim()
    
class TestClass:
    def __init__(self):
        self._rotate_queue = deque()
        self._translate_queue = deque()
        self._pts_queue = deque()
        self._pts = 0
        self._Layer_num = 0


    def create_sublayer(self, _root_layer, strLayerName, orderIndex, bSetAuthoring):
        #layname = 
        self._Layer_num += 1
        identifier1 = LayerUtils.create_sublayer(_root_layer, orderIndex, strLayerName).identifier
        #
        if bSetAuthoring:
            omni.kit.commands.execute("SetEditTargetCommand", layer_identifier=identifier1)


    def prepare_data(self):
        begin = time.time()
        rotation_1 = Gf.Vec3f(0.0, 0.0, 0.0)
        _translate = Gf.Vec3d(0.0, 0.0, 0.0)
        for _ in range(10000):
            self._rotate_queue.append(rotation_1)
            self._translate_queue.append(_translate)
        end = time.time()


        carb.log_info(f"prepare_data elaspe:{end - begin}")


    async def await_flush_save(self):
        carb.log_info("before await_flush_save {time.time()}")
        await omni.kit.app.get_app().next_update_async()
        self.flush_save()
        carb.log_info("end await_flush_save {time.time()}")


    def flush_save(self):
        timecode = 0
        while self._rotate_queue or self._translate_queue:
            if self._rotate_queue:
                f_val = self._rotate_queue.popleft()
                self._rotation_ops.Set(time = timecode, value = f_val)
            if self._translate_queue:
                d_val = self._translate_queue.popleft()
                self._translate_ops.Set(time = timecode, value = d_val)


            timecode += 10


        self._render_update_sub = None


    async def awaitflush(self):
        await omni.kit.app.get_app().next_update_async()
        self.flush_save(self)


    async def flush_save_async(self):
        time0 = time.perf_counter()
        carb.log_info("flush_save_async begin")
        loop = asyncio.get_running_loop()
        # 直接调用同步函数(主线程),但用await asyncio.sleep(0)切分事件循环
        await loop.run_in_executor(None, self.flush_save)
        time1 = time.perf_counter()
        carb.log_info(f"flush_save_async end elaspe:{time1 - time0}")


    def init_stage_camera(self, stage, camera_prim_path):
        self._stage = stage
        self._camera_path = camera_prim_path
        self._camera_prim = UsdGeom.Camera.Get(stage, camera_prim_path).GetPrim()
        xform_ops = UsdGeom.Xformable(self._camera_prim).GetOrderedXformOps()


        for op in xform_ops:
                if op.GetOpType() in [UsdGeom.XformOp.TypeRotateXYZ,
                        UsdGeom.XformOp.TypeRotateXZY,
                        UsdGeom.XformOp.TypeRotateYXZ,
                        UsdGeom.XformOp.TypeRotateYZX,
                        UsdGeom.XformOp.TypeRotateZXY,
                        UsdGeom.XformOp.TypeRotateZYX]:
                    #rotation = op.Get()
                    self._rotation_type = op.GetOpType()
                    self._rotation_ops = op
                    #print(f"rotation is {rotation}")
                elif op.GetOpType() == UsdGeom.XformOp.TypeScale:
                    self._scale_ops  = op
                elif op.GetOpType() == UsdGeom.XformOp.TypeTranslate:
                    self._translate_ops = op


if __name__ == "__main__":
    _stage = omni.usd.get_context().get_stage()
    root_layer = _stage.GetRootLayer()
    prim_path = "/World/Camera"
    new_layer_path = "d:/camera_sublayer.usd"
    runclass = TestClass()
    
    create_prim(_stage, prim_path)
    runclass.init_stage_camera(_stage, prim_path)
    runclass.create_sublayer(root_layer, new_layer_path,0 ,True)
    runclass.prepare_data()
    
    begin = time.time()
    carb.log_info(f"before run coroutine")
    #(1)Async block UI for about 50 seconds
    run_coroutine(runclass.await_flush_save())
    #(2)Also block UI about 50 seconds
    # with ThreadPoolExecutor() as executor:
    #     executor.submit(runclass.flush_save())
    #(3)Sync, same block
    #self.flush_save()


    end = time.time()
    carb.log_info(f"elaspe time is {end-begin} , 10000 ends")
 

 

* 附代码链接:https://github.com/slayersong/OVPerf_Tracy/blob/main/tracy_profiler.py(复制链接至浏览器打开)

复现问题:打开菜单中的 Developer -- Script Editor,打开 tray_profiler.py 文件,然后点击 Run,可以看到创建了一个 camera_sublayer,并且主 UI 卡住了几十秒无响应。

需要注意的是,在 Omniverse USD 的 layer 层级继承覆盖当中,在上层的 Layer 的行为会覆盖下层的 layer,关于 USD layer 层级的关系,请查看本文结尾提供的 DLI 课程链接。

摄影机

然后在 Content Browser 中单击鼠标右键,选择 Edit,可以看到数据成功写入了 USD 文件,只是中间卡顿的时间过长。

摄影机

分析:在遇到 Profiler 的时候不要盲猜,可能是 memory、IO Bound、Compute Bound 或者一些不太能想到的情况,这时候则需要利用专业化的工具进行分析定位,找到问题所在并解决,比如可以利用著名工具 Tracy(https://github.com/wolfpld/tracy),该工具可以分析 CPU / GPU 性能瓶颈,并支持主流 Graphics API:DX、Vulkan、OpenGL、CUDA 等,且 Omniverse 已经把该工具与 Omniverse Kit 进行了集成。因此可以利用 Tracy 去看底层的 CallStack 里什么影响了这个操作,在 Omniverse 当中,Tracy 已经配置好了 Symbol 符号表, 可以看到底层的代码函数调用堆栈,后而寻找具体是什么情况卡住了不正常的几十秒时间。

Tracy 的使用

2.1 操作介绍

Omniverse 已经集成了 Tracy 的开发集成插件:https://docs.omniverse.nvidia.com/extensions/latest/ext_profiler_tracy.html

Tracy 本身是一个著名的分析工具,具体的菜单操作可以参考知乎这个帖子:https://zhuanlan.zhihu.com/p/1915041165033607442

UI 操作的详细讲解可参考如下视频:

Tracy 讲解文档

视频参考:

https://www.bilibili.com/video/BV1or421J7Du/?spm_id_from=333.337.search-card.all.click

文档参考:

https://github.com/CppCon/CppCon2023/blob/main/Presentations/Tracy_Profiler_2024.pdf

2.2 安装

首先打开菜单 Developer -- Extension 搜索,找到 Profiler Tracy 并且安装。

摄影机

然后会出现一个新的 Profiler 菜单,点击 Profiler -- Tracy -- Launch and Connect。

摄影机

Tracy 基本使用操作:

1.    Pause:在实时监测到发生性能瓶颈的事件以后要暂停,否则时间轴会一直向右走

2.    按住鼠标右键可以拖动到你想要的位置

3.    鼠标滚轮:Zoom in / out

2.3 分析问题

运行上述代码,点击 Tracy 中的 Pause 暂停(不暂停 Tracy 会一直记录的一直滚动)。之后按住 Ctrl 和鼠标中间的滚轮,Zoom 缩小操作,可以很容易找到一个最大的耗时,从 11 秒开始到 31 秒,这一个 Frame Render 用了二十几秒(注意:函数的调用堆栈已经正确显示),可以发现卡在了 RenderThread 中的 usd_mutex_wait 函数上面:

摄影机

这样通过 Tracy 的使用就明白了问题卡住的大致原因,简而言之,渲染线程会等待 USD 写入的结束,一直卡在 usd_mutex_wait。

2.4 解决问题

分析:该问题的本质其实是 USD 的写入与修改会非常的慢,这是 USD 的基础架构造成的。

单单针对这个问题解决的方法不复杂,可以思考一下,写入的 Sublayer 其实并不需要实时参与 USD Composite, 因为我们并不需要实时观察到合成结果,可以创建离线的 Sublayer ,等待写入结束以后再自动或者手动把 Sublayer 加入进来,代码如下,看到并没有卡顿这一个过程,那么问题就解决了。

针对此次问题的 Solution 如下:

 

import omni.kit.app
import time
import asyncio
from pxr import Sdf, Usd, UsdGeom, Gf
from omni.kit.usd.layers import LayerUtils, get_layers, LayerEditMode
import omni.kit.commands
from collections import deque
import os
from typing import List, Tuple
from omni.kit.widget.layers.path_utils import PathUtils
import carb
from typing import List, Tuple
from enum import Enum
import asyncio
from pxr import Sdf, Usd, UsdGeom
from pxr import Usd, UsdGeom, Gf
from omni.kit.async_engine import run_coroutine
from concurrent.futures import ALL_COMPLETED, ThreadPoolExecutor, wait


def create_prim(stage, prim_path="/World/Camera"):
    prim = stage.GetPrimAtPath(prim_path)
    if prim and prim.GetTypeName() == "Camera":
        carb.log_info(f"Camera already exists at: {prim_path}")
        return prim
    else:
        camera_prim = UsdGeom.Camera.Define(stage, prim_path)
        camera_prim.AddTranslateOp().Set(Gf.Vec3d(10, 20, 30)) 
        camera_prim.AddRotateXYZOp().Set(Gf.Vec3f(0, 45, 0)) 
        carb.log_info(f"Created new Camera at: {prim_path}")
        return camera_prim.GetPrim()


class TestClass:
    def __init__(self):
        self._rotate_queue = deque()
        self._translate_queue = deque()
        self._pts_queue = deque()
        self._pts = 0
    
    def regis(self):
        self._app = omni.kit.app.get_app()          
        self._render_update_sub = self._app.get_update_event_stream().create_subscription_to_pop(
        self.pre_frame_render, order=-10, name="gm_render_event")
    
    def pre_frame_render(self,e):
        self._pts += 1
        self.get_push_pos_rotate(self._pts)
        
        if self._pts == 1000:
            begin = time.time()
            carb.log_info(f"before run coroutine")
            run_coroutine(self.await_flush_save())
            #self.flush_save()
            carb.log_info(f"end run coroutine")
            end = time.time()
            carb.log_info(f"elaspe time is {end-begin}")
            self._render_update_sub = None
    
    def get_push_pos_rotate(self, pts):
        rotae = self._rotation_ops.Get()
        translate = self._translate_ops.Get()
        
        self._rotate_queue.append(rotae)
        self._translate_queue.append(translate)
        self._pts_queue.append(pts)
    
    def prepare_data(self):
        begin = time.time()
        
        rotation_1 = Gf.Vec3f(0.0, 0.0, 0.0)
        _translate = Gf.Vec3d(0.0, 0.0, 0.0)
        for i in range(500):
            # rotation_1 = Gf.Vec3f(0.0, 0.0, 0.0)
            # _translate = Gf.Vec3d(0.0, 0.0, 0.0)
            rotation_1 = Gf.Vec3f(-253.0, i * (360.0 / 499), 93)  # 99是为了最后一次达到360
            # _translate的xyz从0递增到100
            _translate = Gf.Vec3d( 2124.0,  2124.0,  104)
            
            self._rotate_queue.append(rotation_1)
            self._translate_queue.append(_translate)
        end = time.time()
        
        carb.log_info(f"prepare_data elaspe:{end - begin}")
   
    async def await_flush_save(self):
        carb.log_info("before await_flush_save {time.time()}")
        await omni.kit.app.get_app().next_update_async()
        self.flush_save()
        carb.log_info("end await_flush_save {time.time()}")
    
    def flush_save(self):
        timecode = 0
        while self._rotate_queue or self._translate_queue:
            if self._rotate_queue:
                f_val = self._rotate_queue.popleft()
                #self._rotation_ops.Set(time = timecode, value = f_val)
                self.seq_write_rotate_op.Set(time = timecode, value = f_val)
            if self._translate_queue:
                d_val = self._translate_queue.popleft()
                self.seq_write_translate_op.Set(time = timecode, value = d_val)
                #self._translate_ops.Set(time = timecode, value = d_val)
            
            timecode += 10
        
        self._sub_stage.GetRootLayer().Save()
        self._render_update_sub = None
    
    async def awaitflush(self):
        await omni.kit.app.get_app().next_update_async()
        self.flush_save(self)
    
    async def flush_save_async(self):
        time0 = time.perf_counter()
        carb.log_info("flush_save_async begin")
        loop = asyncio.get_running_loop()
        # 直接调用同步函数(主线程),但用await asyncio.sleep(0)切分事件循环
        await loop.run_in_executor(None, self.flush_save)
        time1 = time.perf_counter()
        carb.log_info(f"flush_save_async end elaspe:{time1 - time0}")
    
    def create_offline_layer(self, layer_base_path, prim_path, bOverride):
        # split name and ext
        name, ext = os.path.splitext(layer_base_path)
        
        index = 1
        new_layer_path = layer_base_path
        
        #If exist create a new path such as basepath_1.usd
        while os.path.exists(new_layer_path):
            new_layer_path = f"{name}_{index}{ext}"
            index += 1
        
        new_layer = Sdf.Layer.CreateNew(new_layer_path)
        
        # 2. 打开该layer对应的Stage(编辑该layer)
        self._sub_stage = Usd.Stage.Open(new_layer)
        
        # 3. 以over方式定义相机Prim(覆盖已有的/world/Camera)
        if bOverride:
            self._seq_camera_prim = self._sub_stage.OverridePrim(prim_path)
            self._seq_camera_prim.SetSpecifier(Sdf.SpecifierOver)
        else:
            self._seq_camera_prim = self._sub_stage.DefinePrim(prim_path)
        
        # 4. 获取或创建Xformable接口,用于添加变换操作
        xformable = UsdGeom.Xformable(self._seq_camera_prim)
        
        # 5. 添加translate和rotateXYZ操作
        self.seq_write_translate_op = xformable.AddTranslateOp()
        self.seq_write_rotate_op = xformable.AddRotateXYZOp()
        
    def init_stage_camera(self, stage, camera_prim_path):
        self._stage = stage
        self._camera_path = camera_prim_path
        self._camera_prim = UsdGeom.Camera.Get(stage, camera_prim_path).GetPrim()
        xform_ops = UsdGeom.Xformable(self._camera_prim).GetOrderedXformOps()
        
        for op in xform_ops:
                if op.GetOpType() in [UsdGeom.XformOp.TypeRotateXYZ,
                        UsdGeom.XformOp.TypeRotateXZY,
                        UsdGeom.XformOp.TypeRotateYXZ,
                        UsdGeom.XformOp.TypeRotateYZX,
                        UsdGeom.XformOp.TypeRotateZXY,
                        UsdGeom.XformOp.TypeRotateZYX]:
                    #rotation = op.Get()
                    self._rotation_type = op.GetOpType()
                    self._rotation_ops = op
                    #print(f"rotation is {rotation}")
                elif op.GetOpType() == UsdGeom.XformOp.TypeScale:
                    self._scale_ops  = op
                elif op.GetOpType() == UsdGeom.XformOp.TypeTranslate:
                    self._translate_ops = op


if __name__ == "__main__":
    _stage = omni.usd.get_context().get_stage()
    
    prim_path = "/World/Camera"
    new_layer_path = "d:\tes303.usda"
    
    runclass = TestClass()
    create_prim(_stage, prim_path)
    runclass.init_stage_camera(_stage, prim_path)
    
    runclass.create_offline_layer(new_layer_path, prim_path, True)
    carb.log_info("create_offline_layer after")
    
    #runclass.regis()
    runclass.prepare_data()
    begin = time.time()
    carb.log_info(f"before run coroutine")
    run_coroutine(runclass.flush_save_async())
    #run_coroutine(runclass.await_flush_save())
    #runclass.flush_save()
    carb.log_info(f"end run coroutine")
    #runclass.flush_save()
    # with ThreadPoolExecutor() as executor:
    #     executor.submit(runclass.flush_save())
    
    end = time.time()
    carb.log_info(f"elaspe time is {end-begin} , 1000 ends")


# class YourClass:
#     def __init__(self):
#         # 初始化队列等
#         pass


#     def flush_save(self):
#         # 这是同步函数,不能改动
#         # 里面调用了Omniverse API,必须在主线程执行
#         print("Begin flush save")
#         time.sleep(1)
#         print("end flush save")


#     async def flush_save_async(self):
#         time0 = time.perf_counter()
#         print("run task begin")
#         loop = asyncio.get_running_loop()
#         # 直接调用同步函数(主线程),但用await asyncio.sleep(0)切分事件循环
#         await loop.run_in_executor(None, self.flush_save)
#         time1 = time.perf_counter()
#         print(f"run task end elaspe:{time1 - time0}")
    
#     async def testawait():
#         pass




# # obj = YourClass()


# # run_coroutine(obj.flush_save_async())
# # print(f"run pass the async")
# # count = 0


# # def pre_frame_render(e):
# #     #print(f"Frame Begin: {app.get_update_number()} {e.payload}, event-type {e.type}, {time.time() * 1000 % 1000000} ")
# #     asyncio.ensure_future(obj.flush_save_async())
# # async def run_task():
# #     time0 = time.perf_counter()
# #     print("run task begin")
# #     await obj.flush_save_async()
# #     print("run task end")
# #     time1 = time.perf_counter()


# #     elapse_time = time1 - time0
# #     print(f"run taks elapse is {elapse_time}")


# # def frame_render(e):
# #     print(f"Frame Render: {app.get_update_number()} {e.payload}, event-type {e.type}, {time.time() * 1000 % 1000000}")


# # def post_frame_render(e):
# #     print(f"Frame End: {app.get_update_number()} {e.payload}, event-type {e.type}, {time.time() * 1000 % 1000000}")


# first_last_event = 1000000


# # pre_update_sub = app.get_pre_update_event_stream().create_subscription_to_pop(
# #     pre_frame_render, order=-first_last_event, name="gm_frame_begin")


# #asyncio.ensure_future(obj.flush_save_async())
 

 

* 附代码链接:https://github.com/slayersong/OVPerf_Tracy/blob/main/solution_%20tracy_profiler.py

但是在一些项目当中一定要实时观察到结果。比如,有很多的数字孪生的工业场景中会存在小车传送带,各种物品都是实时进入到场景管线当中,这其中必定要参与 USD 合成。

所以这里介绍一个 Omniverse 对 USD 进行重构的基本概念 Fabric,USDRT(USDRT 是 Fabric 的 API),NVIDIA 在 Omniverse 当中开发了 Fabric 组件专门处理 USD 实时更改缓慢的问题:

https://docs.omniverse.nvidia.com/kit/docs/usdrt/latest/docs/usd_fabric_usdrt.html

通过这个官方文档的图也验证了刚才的结论:Render 线程会等待 USD 的合成结果 Composed 后进行渲染。

摄影机

结束:如果单解决这个问题其实并不复杂,但是其中需要用到很多的基础知识,包括 USD 的合成机制、多线程开发、遇到问题如何去利用工具定位分析等。后面我们将会对 USDRT 与 Fabric 进行更细致的讲解,包括代码的开发使用和 Omniverse 中其他性能工具的使用教程。也希望更多的朋友可以分享在 USD 开发过程当中的心得体会。

附录:

关于前面提到的 USD 的基本开发教程,包括 USD 合成机制,USD 基本动画 TimeSampler 等:

文案提供和技术支持:

宋毅明  

NVIDIA Omniverse & OpenUSD 开发者关系经理

*与 NVIDIA 产品相关的图片或视频(完整或部分)的版权均归 NVIDIA Corporation 所有。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分