FPS(帧率),即frames per second。

目前,帧率统计软件使用的信息来源主要有两个: 一个是基于dumpsys SurfaceFlinger --latency layer-name;另一个是基于dumpsys gfxinfo

本文不说怎么使用上面两个方法来统计帧率,主要说dumpsys SurfaceFlinger --latency layer-name的数据来源。

想知道怎么统计帧率的,可以参考下面两篇文章:

android 端监控方案分享 · TesterHome

介绍下基于 systrace gfx 统计帧率方案的实现过程 · TesterHome

执行dumpsys SurfaceFlinger --latency layer-name命令后,产生的输出来源于FrameTracker::dumpStats(std::string& result)方法。

frameworks/native/services/surfaceflinger/FrameTracker.cpp

void FrameTracker::dumpStats(std::string& result) const {Mutex::Autolock lock(mMutex);processFencesLocked();const size_t o = mOffset;for (size_t i = 1; i < NUM_FRAME_RECORDS; i++) {const size_t index = (o+i) % NUM_FRAME_RECORDS;base::StringAppendF(&result, "%" PRId64 "\t%" PRId64 "\t%" PRId64 "\n",mFrameRecords[index].desiredPresentTime,mFrameRecords[index].actualPresentTime,mFrameRecords[index].frameReadyTime);}result.append("\n");
}

先说结论:desiredPresentTime,actualPresentTime,frameReadyTime分别代表:

  • desiredPresentTime:queueBuffer 的时间戳
  • actualPresentTime:present fence signal 的时间戳
  • frameReadyTime:acquire fence signal 的时间戳

Fence的类型

Synchronization Framework

The Hardware Composer handles three types of sync fences:

  • Acquire fences are passed along with input buffers to the setLayerBuffer and setClientTarget calls. These represent a pending write into the buffer and must signal before the SurfaceFlinger or the HWC attempts to read from the associated buffer to perform composition.
  • Release fences are retrieved after the call to presentDisplay using the getReleaseFences call. These represent a pending read from the previous buffer on the same layer. A release fence signals when the HWC is no longer using the previous buffer because the current buffer has replaced the previous buffer on the display. Release fences are passed back to the app along with the previous buffers that will be replaced during the current composition. The app must wait until a release fence signals before writing new contents into the buffer that was returned to them.
  • Present fences are returned, one per frame, as part of the call to presentDisplay. Present fences represent when the composition of this frame has completed, or alternately, when the composition result of the prior frame is no longer needed. For physical displays, presentDisplay returns present fences when the current frame appears on the screen. After present fences are returned, it’s safe to write to the SurfaceFlinger target buffer again, if applicable. For virtual displays, present fences are returned when it’s safe to read from the output buffer.

在 Android 里面,总共有三类 fence —— acquire fence,release fence 和 present fence。其中,acquire fence 和 release fence 隶属于 Layer,present fence 隶属于帧(即 Layers):

acquire fence :App 将 Buffer 通过 queueBuffer() 还给 BufferQueue 的时候,此时该 Buffer 的 GPU 侧其实是还没有完成的,此时会带上一个 fence,这个 fence 就是 acquire fence。当 SurfaceFlinger/ HWC 要读取 Buffer 以进行合成操作的时候,需要等 acquire fence 释放之后才行。

release fence :当 App 通过 dequeueBuffer() 从 BufferQueue 申请 Buffer,要对 Buffer 进行绘制的时候,需要保证 HWC 已经不再需要这个 Buffer 了,即需要等 release fence signal 才能对 Buffer 进行写操作。

present fence :present fence 在 HWC1 的时候称为 retire fence,在 HWC2 中改名为 present fence。当前帧成功显示到屏幕的时候,present fence 就会 signal。

desiredPresentTime和frameReadyTime来自哪里(基于安卓12源码分析)

frameworks/native/services/surfaceflinger/FrameTracker.h

    // setDesiredPresentTime sets the time at which the current frame// should be presented to the user under ideal (i.e. zero latency)// conditions.//理想条件下(即零延迟)呈现给用户的时间点。void setDesiredPresentTime(nsecs_t desiredPresentTime);// setFrameReadyTime sets the time at which the current frame became ready// to be presented to the user.  For example, if the frame contents is// being written to memory by some asynchronous hardware, this would be// the time at which those writes completed.void setFrameReadyTime(nsecs_t readyTime);// setFrameReadyFence sets the fence that is used to get the time at which// the current frame became ready to be presented to the user.void setFrameReadyFence(std::shared_ptr<FenceTime>&& readyFence);// setActualPresentTime sets the timestamp at which the current frame became// visible to the user.void setActualPresentTime(nsecs_t displayTime);// setActualPresentFence sets the fence that is used to get the time// at which the current frame became visible to the user.void setActualPresentFence(const std::shared_ptr<FenceTime>& fence);

frameworks/native/services/surfaceflinger/BufferLayer.cpp

bool BufferLayer::onPostComposition(const DisplayDevice* display,const std::shared_ptr<FenceTime>& glDoneFence,const std::shared_ptr<FenceTime>& presentFence,const CompositorTiming& compositorTiming) {......// Update mFrameTracker.nsecs_t desiredPresentTime = mBufferInfo.mDesiredPresentTime;//关键点1:设置desiredPresentTimemFrameTracker.setDesiredPresentTime(desiredPresentTime);......std::shared_ptr<FenceTime> frameReadyFence = mBufferInfo.mFenceTime;if (frameReadyFence->isValid()) {//关键点2:设置frameReadyFencemFrameTracker.setFrameReadyFence(std::move(frameReadyFence));} else {//纯软件绘制才走这里,frameReadyTime和desiredPresentTime就一样了// There was no fence for this frame, so assume that it was ready// to be presented at the desired present time.mFrameTracker.setFrameReadyTime(desiredPresentTime);}if (display) {const Fps refreshRate = display->refreshRateConfigs().getCurrentRefreshRate().getFps();const std::optional<Fps> renderRate =mFlinger->mScheduler->getFrameRateOverride(getOwnerUid());if (presentFence->isValid()) {//关键点3,设置actualPresentFencemFlinger->mTimeStats->setPresentFence(layerId, mCurrentFrameNumber, presentFence,refreshRate, renderRate,frameRateToSetFrameRateVotePayload(mDrawingState.frameRate),getGameMode());mFlinger->mFrameTracer->traceFence(layerId, getCurrentBufferId(), mCurrentFrameNumber,presentFence,FrameTracer::FrameEvent::PRESENT_FENCE);mFrameTracker.setActualPresentFence(std::shared_ptr<FenceTime>(presentFence));} else if (const auto displayId = PhysicalDisplayId::tryCast(display->getId());displayId && mFlinger->getHwComposer().isConnected(*displayId)) {// The HWC doesn't support present fences, so use the refresh// timestamp instead.const nsecs_t actualPresentTime = display->getRefreshTimestamp();mFlinger->mTimeStats->setPresentTime(layerId, mCurrentFrameNumber, actualPresentTime,refreshRate, renderRate,frameRateToSetFrameRateVotePayload(mDrawingState.frameRate),getGameMode());mFlinger->mFrameTracer->traceTimestamp(layerId, getCurrentBufferId(),mCurrentFrameNumber, actualPresentTime,FrameTracer::FrameEvent::PRESENT_FENCE);//不支持fence,才设置成屏幕刷新周期时间戳mFrameTracker.setActualPresentTime(actualPresentTime);}}mFrameTracker.advanceFrame();mBufferInfo.mFrameLatencyNeeded = false;return true;
}

从上面代码来看,desiredPresentTime和frameReadyFence与mBufferInfo有关,接下来看一下它是在哪里赋值的。

frameworks/native/services/surfaceflinger/BufferQueueLayer.cpp

void BufferQueueLayer::gatherBufferInfo() {BufferLayer::gatherBufferInfo();//与desiredPresentTime有关mBufferInfo.mDesiredPresentTime = mConsumer->getTimestamp();//与frameReadyFence有关mBufferInfo.mFenceTime = mConsumer->getCurrentFenceTime();mBufferInfo.mFence = mConsumer->getCurrentFence();
......
}

继续往下追踪,看mConsumer的mCurrentTimestamp和mCurrentFenceTime是在哪里赋值的。

frameworks/native/services/surfaceflinger/BufferLayerConsumer.cpp

//获取mCurrentTimestamp
nsecs_t BufferLayerConsumer::getTimestamp() {BLC_LOGV("getTimestamp");Mutex::Autolock lock(mMutex);return mCurrentTimestamp;
}//获取mCurrentFenceTime
std::shared_ptr<FenceTime> BufferLayerConsumer::getCurrentFenceTime() const {Mutex::Autolock lock(mMutex);return mCurrentFenceTime;
}status_t BufferLayerConsumer::updateAndReleaseLocked(const BufferItem& item,PendingRelease* pendingRelease) {status_t err = NO_ERROR;int slot = item.mSlot;BLC_LOGV("updateAndRelease: (slot=%d buf=%p) -> (slot=%d buf=%p)", mCurrentTexture,(mCurrentTextureBuffer != nullptr && mCurrentTextureBuffer->getBuffer() != nullptr)? mCurrentTextureBuffer->getBuffer()->handle: 0,slot, mSlots[slot].mGraphicBuffer->handle);// Hang onto the pointer so that it isn't freed in the call to// releaseBufferLocked() if we're in shared buffer mode and both buffers are// the same.std::shared_ptr<renderengine::ExternalTexture> nextTextureBuffer;{std::lock_guard<std::mutex> lock(mImagesMutex);nextTextureBuffer = mImages[slot];}// release old bufferif (mCurrentTexture != BufferQueue::INVALID_BUFFER_SLOT) {if (pendingRelease == nullptr) {status_t status =releaseBufferLocked(mCurrentTexture, mCurrentTextureBuffer->getBuffer());if (status < NO_ERROR) {BLC_LOGE("updateAndRelease: failed to release buffer: %s (%d)", strerror(-status),status);err = status;// keep going, with error raised [?]}} else {pendingRelease->currentTexture = mCurrentTexture;pendingRelease->graphicBuffer = mCurrentTextureBuffer->getBuffer();pendingRelease->isPending = true;}}// Update the BufferLayerConsumer state.mCurrentTexture = slot;mCurrentTextureBuffer = nextTextureBuffer;mCurrentCrop = item.mCrop;mCurrentTransform = item.mTransform;mCurrentScalingMode = item.mScalingMode;//mCurrentTimestamp在这里赋值mCurrentTimestamp = item.mTimestamp;mCurrentDataSpace = static_cast<ui::Dataspace>(item.mDataSpace);mCurrentHdrMetadata = item.mHdrMetadata;mCurrentFence = item.mFence;//mCurrentFenceTime在这里赋值mCurrentFenceTime = item.mFenceTime;mCurrentFrameNumber = item.mFrameNumber;mCurrentTransformToDisplayInverse = item.mTransformToDisplayInverse;mCurrentSurfaceDamage = item.mSurfaceDamage;mCurrentApi = item.mApi;computeCurrentTransformMatrixLocked();return err;
}status_t BufferLayerConsumer::updateTexImage(BufferRejecter* rejecter, nsecs_t expectedPresentTime,bool* autoRefresh, bool* queuedBuffer,uint64_t maxFrameNumber) {ATRACE_CALL();BLC_LOGV("updateTexImage");Mutex::Autolock lock(mMutex);if (mAbandoned) {BLC_LOGE("updateTexImage: BufferLayerConsumer is abandoned!");return NO_INIT;}BufferItem item;// Acquire the next buffer.// In asynchronous mode the list is guaranteed to be one buffer// deep, while in synchronous mode we use the oldest buffer.status_t err = acquireBufferLocked(&item, expectedPresentTime, maxFrameNumber);if (err != NO_ERROR) {if (err == BufferQueue::NO_BUFFER_AVAILABLE) {err = NO_ERROR;} else if (err == BufferQueue::PRESENT_LATER) {// return the error, without logging} else {BLC_LOGE("updateTexImage: acquire failed: %s (%d)", strerror(-err), err);}return err;}if (autoRefresh) {*autoRefresh = item.mAutoRefresh;}if (queuedBuffer) {*queuedBuffer = item.mQueuedBuffer;}// We call the rejecter here, in case the caller has a reason to// not accept this buffer.  This is used by SurfaceFlinger to// reject buffers which have the wrong sizeint slot = item.mSlot;if (rejecter && rejecter->reject(mSlots[slot].mGraphicBuffer, item)) {releaseBufferLocked(slot, mSlots[slot].mGraphicBuffer);return BUFFER_REJECTED;}// Release the previous buffer.//这里调用err = updateAndReleaseLocked(item, &mPendingRelease);if (err != NO_ERROR) {return err;}return err;
}

从上面代码来看,mCurrentTimestamp和mCurrentFenceTime是updateAndReleaseLocked里面赋值的,而其又被updateTexImage调用,这个函数在systrace可视化页面很常见了,接下来分析与他们相关的mTimestamp和mFenceTime。

frameworks/native/libs/gui/include/gui/BufferItem.h

 // mTimestamp is the current timestamp for this buffer slot. This gets// to set by queueBuffer each time this slot is queued. This value// is guaranteed to be monotonically increasing for each newly// acquired buffer.
//从这里的注释已经可以知道它是queueBuffer时的时间戳int64_t mTimestamp;// The std::shared_ptr<FenceTime> wrapper around mFence.std::shared_ptr<FenceTime> mFenceTime{FenceTime::NO_FENCE};

frameworks/native/libs/gui/BufferQueueProducer.cpp

status_t BufferQueueProducer::queueBuffer(int slot,const QueueBufferInput &input, QueueBufferOutput *output) {ATRACE_CALL();ATRACE_BUFFER_INDEX(slot);int64_t requestedPresentTimestamp;bool isAutoTimestamp;android_dataspace dataSpace;Rect crop(Rect::EMPTY_RECT);int scalingMode;uint32_t transform;uint32_t stickyTransform;sp<Fence> acquireFence;bool getFrameTimestamps = false;//关键点input.deflate(&requestedPresentTimestamp, &isAutoTimestamp, &dataSpace,&crop, &scalingMode, &transform, &acquireFence, &stickyTransform,&getFrameTimestamps);const Region& surfaceDamage = input.getSurfaceDamage();const HdrMetadata& hdrMetadata = input.getHdrMetadata();if (acquireFence == nullptr) {BQ_LOGE("queueBuffer: fence is NULL");return BAD_VALUE;}//关键点auto acquireFenceTime = std::make_shared<FenceTime>(acquireFence);......sp<IConsumerListener> frameAvailableListener;sp<IConsumerListener> frameReplacedListener;int callbackTicket = 0;uint64_t currentFrameNumber = 0;BufferItem item;{ // Autolock scope......//这里赋值item.mTimestamp = requestedPresentTimestamp;item.mIsAutoTimestamp = isAutoTimestamp;item.mDataSpace = dataSpace;item.mHdrMetadata = hdrMetadata;item.mFrameNumber = currentFrameNumber;item.mSlot = slot;item.mFence = acquireFence;//这里赋值item.mFenceTime = acquireFenceTime;......ATRACE_INT(mCore->mConsumerName.string(),static_cast<int32_t>(mCore->mQueue.size()));
#ifndef NO_BINDERmCore->mOccupancyTracker.registerOccupancyChange(mCore->mQueue.size());
#endif// Take a ticket for the callback functionscallbackTicket = mNextCallbackTicket++;VALIDATE_CONSISTENCY();} // Autolock scope......return NO_ERROR;
}

frameworks/native/libs/gui/include/gui/IGraphicBufferProducer.h

// timestamp - a monotonically increasing value in nanoseconds// isAutoTimestamp - if the timestamp was synthesized at queue time// dataSpace - description of the contents, interpretation depends on format// crop - a crop rectangle that's used as a hint to the consumer// scalingMode - a set of flags from NATIVE_WINDOW_SCALING_* in <window.h>// transform - a set of flags from NATIVE_WINDOW_TRANSFORM_* in <window.h>// acquireFenceTime相关// fence - a fence that the consumer must wait on before reading the buffer,//         set this to Fence::NO_FENCE if the buffer is ready immediately// sticky - the sticky transform set in Surface (only used by the LEGACY//          camera mode).// getFrameTimestamps - whether or not the latest frame timestamps//                      should be retrieved from the consumer.// slot - the slot index to queue. This is used only by queueBuffers().//        queueBuffer() ignores this value and uses the argument `slot`//        instead.inline QueueBufferInput(int64_t _timestamp, bool _isAutoTimestamp,android_dataspace _dataSpace, const Rect& _crop,int _scalingMode, uint32_t _transform, const sp<Fence>& _fence,uint32_t _sticky = 0, bool _getFrameTimestamps = false,int _slot = -1): timestamp(_timestamp), isAutoTimestamp(_isAutoTimestamp),dataSpace(_dataSpace), crop(_crop), scalingMode(_scalingMode),transform(_transform), stickyTransform(_sticky),fence(_fence), surfaceDamage(),getFrameTimestamps(_getFrameTimestamps), slot(_slot) { }inline void deflate(int64_t* outTimestamp, bool* outIsAutoTimestamp,android_dataspace* outDataSpace,Rect* outCrop, int* outScalingMode,uint32_t* outTransform, sp<Fence>* outFence,uint32_t* outStickyTransform = nullptr,bool* outGetFrameTimestamps = nullptr,int* outSlot = nullptr) const {//mTimestamp相关*outTimestamp = timestamp;*outIsAutoTimestamp = bool(isAutoTimestamp);*outDataSpace = dataSpace;*outCrop = crop;*outScalingMode = scalingMode;*outTransform = transform;//acquireFenceTime相关*outFence = fence;if (outStickyTransform != nullptr) {*outStickyTransform = stickyTransform;}if (outGetFrameTimestamps) {*outGetFrameTimestamps = getFrameTimestamps;}if (outSlot) {*outSlot = slot;}}

frameworks/native/libs/gui/Surface.cpp

void Surface::getQueueBufferInputLocked(android_native_buffer_t* buffer, int fenceFd,nsecs_t timestamp, IGraphicBufferProducer::QueueBufferInput* out) {bool isAutoTimestamp = false;//desiredPresentTime的真正来源if (timestamp == NATIVE_WINDOW_TIMESTAMP_AUTO) {timestamp = systemTime(SYSTEM_TIME_MONOTONIC);isAutoTimestamp = true;ALOGV("Surface::queueBuffer making up timestamp: %.2f ms",timestamp / 1000000.0);}// Make sure the crop rectangle is entirely inside the buffer.Rect crop(Rect::EMPTY_RECT);mCrop.intersect(Rect(buffer->width, buffer->height), &crop);//frameReadyFence真正来源sp<Fence> fence(fenceFd >= 0 ? new Fence(fenceFd) : Fence::NO_FENCE);//实例化QueueBufferInput,并传递timestamp和fenceIGraphicBufferProducer::QueueBufferInput input(timestamp, isAutoTimestamp,static_cast<android_dataspace>(mDataSpace), crop, mScalingMode,mTransform ^ mStickyTransform, fence, mStickyTransform,mEnableFrameTimestamps);// we should send HDR metadata as needed if this becomes a bottleneckinput.setHdrMetadata(mHdrMetadata);......
}int Surface::queueBuffer(android_native_buffer_t* buffer, int fenceFd) {ATRACE_CALL();ALOGV("Surface::queueBuffer");......IGraphicBufferProducer::QueueBufferOutput output;IGraphicBufferProducer::QueueBufferInput input;//调用getQueueBufferInputLocked(buffer, fenceFd, mTimestamp, &input);sp<Fence> fence = input.fence;nsecs_t now = systemTime();//调用status_t err = mGraphicBufferProducer->queueBuffer(i, input, &output);mLastQueueDuration = systemTime() - now;if (err != OK)  {ALOGE("queueBuffer: error queuing buffer, %d", err);}onBufferQueuedLocked(i, fence, output);return err;
}

getQueueBufferInputLocked最终被 Surface::queueBuffer调用。

至此,已经分析完 desiredPresentTime 和 frameReadyTime 的最终来源了,即:

actualPresentTime指代的是什么

我就不分析这个了,流程都是差不多的,详情参考:

或许是迄今为止第一篇讲解 fps 计算原理的文章吧 - 掘金

其作者提到,计算一个 App 的 fps 的原理就是:

统计在一秒内该 App 往屏幕刷了多少帧,而在 Android 的世界里,每一帧显示到屏幕的标志是:present fence signal 了,因此计算 App 的 fps 就可以转换为:一秒内 App 的 Layer 有多少个有效 present fence signal 了(这里有效 present fence 是指,在本次 VSYNC 中该 Layer 有更新的 present fence)

参考文章

安卓帧率计算方案和背后原理剖析

或许是迄今为止第一篇讲解 fps 计算原理的文章吧 - 掘金

安卓帧率FPS计算原理相关推荐

  1. 帧率(FPS)计算的六种方法总结

    帧率(FPS)计算是游戏编程中常见的一个话题.大体来说,总共有如下六种方法: 一.固定时间帧数法 帧率计算的公式为: fps = frameNum / elapsedTime; 如果记录固定时间内的帧 ...

  2. opengl计算帧率_或许是迄今为止第一篇讲解 fps 计算原理的文章吧

    前言 fps,是 frames per second 的简称,也就是我们常说的"帧率".在游戏领域中,fps 作为衡量游戏性能的基础指标,对于游戏开发和手机 vendor 厂商都是 ...

  3. 或许是迄今为止第一篇讲解 fps 计算原理的文章吧

    前言 fps,是 frames per second 的简称,也就是我们常说的"帧率".在游戏领域中,fps 作为衡量游戏性能的基础指标,对于游戏开发和手机 vendor 厂商都是 ...

  4. 帧率(FPS)计算的几种方法总结

    帧率(FPS, frame per second)计算是游戏编程中常见的一个话题,因为表现在画面刷新与视觉感官上,所以相对而言,帧率非常影响用户体验.这也是很多大型3D游戏所要提升的重要点,意味着你要 ...

  5. 【Camera专题】Sprd-Camera帧率fps的计算及拍照闪红问题的解决

    吐槽 换了新公司,一上来就面对两个比较棘手的问题,2个问题都是拖了几个月没有解决,跟展讯那边沟通迟迟没有解决方案. 原本是做MTK平台的,到了这边需要做展讯平台和高通平台. 证明能力的时候到了! 一周 ...

  6. 【使用opencv方法计算帧率fps】

    getTickCount() 返回从操作系统启动到目前为止所经过的记时周期数 也就是当前的Tick的数量. getTickFrequency() 返回CPU的频率. 计算耗时 t1 = getTick ...

  7. opengl计算帧率_unity如何计算帧率FPS

    在使用unity开发过程中,许多时候需要显示当前项目的帧率FPS,用于观察项目的流程度,那么如何计算FPS呢?请看下面代码演示: public class FPSShow:MonoBehaviour ...

  8. Unity3D-计算帧率FPS

    网上有很多计算FPS的方法,一般计算没有达到百分之百准确的帧率,只有更接近实际帧率的计算方式. 下面是本人测试多种方法之后觉得比较接近实际帧率的计算方式. public class FPS : Mon ...

  9. 面试:如何侦测应用的帧率FPS

    有两种常用方式 360 ArgusAPM类实现方式: 监测Choreographer两次Vsync时间差 BlockCanary的实现方式:监测UI线程单条Message执行时间 方案一:使用Chor ...

最新文章

  1. 硬核!两个博士结婚,接亲时新娘给新郎摆了盘棋局:你赢了再娶我!
  2. 程序员需要谨记的9个安全编码规则【转载】
  3. go语言游戏编程-Ebiten实现画面的填充
  4. ch341a编程和ttl刷机区别_土豪金CH341a编程器 开箱晒物
  5. Neo4j Java Rest绑定入门(Heroku部署)
  6. axios异步请求数据的简单使用
  7. java pdf增删改查_如何利用Java代码操作索引库?
  8. c语言选择法排序案例,谁能给我一个c语言选择排序法的简单例子
  9. Spring : spring基于xml配置Bean
  10. 编译32位_实战经验:在Windows平台编译x264
  11. 《当程序员的那些狗日日子》三
  12. 【致敬童年】Funcode实现坦克大战
  13. 植物大战僵尸修改器 - 简易版
  14. git删除远程创库命令
  15. oracle从11.0.2.4.0打PSU 11.0.2.4.8
  16. Ubuntu20.04安装微信的方法
  17. IOS小知识点5之内存警告、循环引用、交叉引用
  18. Git 【fatal: The remote end hung up unexpectedly 问题】
  19. 中国地质大学英语语音学习笔记(一):元音(单元音,双元音,三元音)
  20. python 自动生成问卷表的软件的设计与实现 毕业设计源码291138

热门文章

  1. mybatis-plus/mybatis的组件们——拦截器、字段填充器、类型处理器、表名替换、SqlInjector(联合主键处理)
  2. 【游戏开发问题】Unity自己莫名其妙添加了ACCESS_FINE_LOCATION权限的问题
  3. idea出现NBSP
  4. 【报告分享】2021年中国白领人群消费及职场社交研究报告-艾瑞(附下载)
  5. 【问题解决】Ubuntu18.04 网络图标不见了,显示有线未托管
  6. Jupyter登录密码问题
  7. 【HNOI2014】世界树
  8. 开发新产品离不开CRM需求分析
  9. 小满nestjs(第十五章 nestjs 和 RxJs)
  10. sysbench 介绍