五湖四海打一生肖

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

UI 卡顿问题复现及分析

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

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

在外部摄影机记录原始的绿幕影片素材的时候,点击开始 / 结束分别会触发一个时间码 (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 的源代码如下:

importomni.kit.app
importtime
importasyncio
frompxrimportSdf, Usd, UsdGeom, Gf
fromomni.kit.usd.layersimportLayerUtils, get_layers, LayerEditMode
importomni.kit.commands
fromcollectionsimportdeque
importos
fromtypingimportList,Tuple
fromomni.kit.widget.layers.path_utilsimportPathUtils
importcarb
fromtypingimportList,Tuple
fromenumimportEnum
importasyncio
frompxrimportSdf, Usd, UsdGeom
frompxrimportUsd, UsdGeom, Gf
fromomni.kit.async_engineimportrun_coroutine
fromconcurrent.futuresimportALL_COMPLETED, ThreadPoolExecutor, wait


defcreate_prim(stage, prim_path="/World/Camera"):
  prim = stage.GetPrimAtPath(prim_path)
 ifprimandprim.GetTypeName() =="Camera":
    carb.log_info(f"Camera already exists at:{prim_path}")
   returnprim
 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}")
   returncamera_prim.GetPrim()
 
classTestClass:
 def__init__(self):
    self._rotate_queue = deque()
    self._translate_queue = deque()
    self._pts_queue = deque()
    self._pts =0
    self._Layer_num =0


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


 defprepare_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_inrange(10000):
      self._rotate_queue.append(rotation_1)
      self._translate_queue.append(_translate)
    end = time.time()


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


 asyncdefawait_flush_save(self):
    carb.log_info("before await_flush_save {time.time()}")
   awaitomni.kit.app.get_app().next_update_async()
    self.flush_save()
    carb.log_info("end await_flush_save {time.time()}")


 defflush_save(self):
    timecode =0
   whileself._rotate_queueorself._translate_queue:
     ifself._rotate_queue:
        f_val = self._rotate_queue.popleft()
        self._rotation_ops.Set(time = timecode, value = f_val)
     ifself._translate_queue:
        d_val = self._translate_queue.popleft()
        self._translate_ops.Set(time = timecode, value = d_val)


      timecode +=10


    self._render_update_sub =None


 asyncdefawaitflush(self):
   awaitomni.kit.app.get_app().next_update_async()
    self.flush_save(self)


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


 definit_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()


   foropinxform_ops:
       ifop.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}")
       elifop.GetOpType() == UsdGeom.XformOp.TypeScale:
          self._scale_ops = op
       elifop.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 文件,只是中间卡顿的时间过长。

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

分析:在遇到 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。

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

Tracy 基本使用操作:

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

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

3. 鼠标滚轮:Zoom in / out

2.3 分析问题

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

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

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

2.4 解决问题

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

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

针对此次问题的 Solution 如下:

importomni.kit.app
importtime
importasyncio
frompxrimportSdf, Usd, UsdGeom, Gf
fromomni.kit.usd.layersimportLayerUtils, get_layers, LayerEditMode
importomni.kit.commands
fromcollectionsimportdeque
importos
fromtypingimportList,Tuple
fromomni.kit.widget.layers.path_utilsimportPathUtils
importcarb
fromtypingimportList,Tuple
fromenumimportEnum
importasyncio
frompxrimportSdf, Usd, UsdGeom
frompxrimportUsd, UsdGeom, Gf
fromomni.kit.async_engineimportrun_coroutine
fromconcurrent.futuresimportALL_COMPLETED, ThreadPoolExecutor, wait


defcreate_prim(stage, prim_path="/World/Camera"):
  prim = stage.GetPrimAtPath(prim_path)
 ifprimandprim.GetTypeName() =="Camera":
    carb.log_info(f"Camera already exists at:{prim_path}")
   returnprim
 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}")
   returncamera_prim.GetPrim()


classTestClass:
 def__init__(self):
    self._rotate_queue = deque()
    self._translate_queue = deque()
    self._pts_queue = deque()
    self._pts =0
 
 defregis(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")
 
 defpre_frame_render(self,e):
    self._pts +=1
    self.get_push_pos_rotate(self._pts)
   
   ifself._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
 
 defget_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)
 
 defprepare_data(self):
    begin = time.time()
   
    rotation_1 = Gf.Vec3f(0.0,0.0,0.0)
    _translate = Gf.Vec3d(0.0,0.0,0.0)
   foriinrange(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}")
 
 asyncdefawait_flush_save(self):
    carb.log_info("before await_flush_save {time.time()}")
   awaitomni.kit.app.get_app().next_update_async()
    self.flush_save()
    carb.log_info("end await_flush_save {time.time()}")
 
 defflush_save(self):
    timecode =0
   whileself._rotate_queueorself._translate_queue:
     ifself._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)
     ifself._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
 
 asyncdefawaitflush(self):
   awaitomni.kit.app.get_app().next_update_async()
    self.flush_save(self)
 
 asyncdefflush_save_async(self):
    time0 = time.perf_counter()
    carb.log_info("flush_save_async begin")
    loop = asyncio.get_running_loop()
   # 直接调用同步函数(主线程),但用await asyncio.sleep(0)切分事件循环
   awaitloop.run_in_executor(None, self.flush_save)
    time1 = time.perf_counter()
    carb.log_info(f"flush_save_async end elaspe:{time1 - time0}")
 
 defcreate_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
   whileos.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)
   ifbOverride:
      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()
   
 definit_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()
   
   foropinxform_ops:
       ifop.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}")
       elifop.GetOpType() == UsdGeom.XformOp.TypeScale:
          self._scale_ops = op
       elifop.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 后进行渲染。

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

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

附录:

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

文案提供和技术支持:

宋毅明

NVIDIA Omniverse & OpenUSD 开发者关系经理

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

继续浏览有关 哪家渐变Trek日本 的文章