| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/viz/service/debugger/viz_debugger.h" |
| |
| #include <algorithm> |
| #include <atomic> |
| #include <string> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/compiler_specific.h" |
| #include "base/no_destructor.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/values.h" |
| #include "skia/ext/codec_utils.h" |
| #include "third_party/skia/include/core/SkStream.h" |
| #include "third_party/skia/include/core/SkSwizzle.h" |
| |
| #if VIZ_DEBUGGER_IS_ON() |
| |
| #include "base/threading/platform_thread.h" |
| #include "base/threading/thread_id_name_manager.h" |
| |
| namespace viz { |
| |
| // Version the protocol in case we ever want or need backwards compatibility |
| // support. |
| static const int kVizDebuggerVersion = 1; |
| |
| std::atomic<bool> VizDebugger::enabled_ = false; |
| |
| VizDebugger::BufferInfo::BufferInfo() = default; |
| VizDebugger::BufferInfo::~BufferInfo() = default; |
| VizDebugger::BufferInfo::BufferInfo(const BufferInfo& a) = default; |
| |
| VizDebugger* VizDebugger::GetInstance() { |
| static base::NoDestructor<VizDebugger> g_debugger; |
| return g_debugger.get(); |
| } |
| |
| VizDebugger::FilterBlock::FilterBlock(const std::string file_str, |
| const std::string func_str, |
| const std::string anno_str, |
| bool is_active, |
| bool is_enabled) |
| : file(std::move(file_str)), |
| func(std::move(func_str)), |
| anno(std::move(anno_str)), |
| active(is_active), |
| enabled(is_enabled) {} |
| |
| VizDebugger::FilterBlock::~FilterBlock() = default; |
| |
| VizDebugger::FilterBlock::FilterBlock(const FilterBlock& other) = default; |
| |
| base::Value::Dict VizDebugger::CallSubmitCommon::GetDictionaryValue() const { |
| return base::Value::Dict() |
| .Set("drawindex", draw_index) |
| .Set("source_index", source_index) |
| // Since this is only for debugging, it's ok for thread ids to be |
| // truncated. |
| .Set("thread_id", static_cast<int32_t>(thread_id)) |
| .Set("option", |
| base::Value::Dict() |
| .Set("color", base::StringPrintf("#%02x%02x%02x", option.color_r, |
| option.color_g, option.color_b)) |
| .Set("alpha", option.color_a)); |
| } |
| |
| VizDebugger::StaticSource::StaticSource(const char* anno_name, |
| const char* file_name, |
| int file_line, |
| const char* func_name) |
| : anno(anno_name), file(file_name), func(func_name), line(file_line) { |
| VizDebugger::GetInstance()->RegisterSource(this); |
| } |
| |
| VizDebugger::VizDebugger() |
| : gpu_thread_task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) { |
| enabled_.store(false); |
| } |
| |
| VizDebugger::~VizDebugger() = default; |
| |
| void VizDebugger::SubmitBuffer(int buff_id, VizDebugger::BufferInfo&& buffer) { |
| read_write_lock_.WriteLock(); |
| VizDebugger::Buffer buff; |
| buff.id = buff_id; |
| buff.buffer_info = std::move(buffer); |
| buffers_.emplace_back(buff); |
| read_write_lock_.WriteUnLock(); |
| } |
| |
| base::Value VizDebugger::FrameAsJson(const uint64_t counter, |
| const gfx::Size& window_pix, |
| base::TimeTicks time_ticks) { |
| // TODO(petermcneeley): When we move to multithread we need to do something |
| // like an atomic swap here. Currently all multithreading concerns are handled |
| // by having a lock around the |json_frame_output_| object. |
| submission_count_ = 0; |
| |
| auto global_dict = |
| base::Value::Dict() |
| .Set("version", kVizDebuggerVersion) |
| .Set("frame", base::NumberToString(counter)) |
| .Set("windowx", window_pix.width()) |
| .Set("windowy", window_pix.height()) |
| .Set("time", base::NumberToString( |
| time_ticks.since_origin().InMicroseconds())); |
| |
| base::Value::List new_sources; |
| for (size_t i = last_sent_source_count_; i < sources_.size(); i++) { |
| const StaticSource* each = sources_[i]; |
| |
| new_sources.Append(base::Value::Dict() |
| .Set("file", each->file) |
| .Set("line", each->line) |
| .Set("func", each->func) |
| .Set("anno", each->anno) |
| .Set("index", each->reg_index)); |
| } |
| |
| // Remote connection will now have acknowledged all the new sources. |
| last_sent_source_count_ = sources_.size(); |
| global_dict.Set("new_sources", std::move(new_sources)); |
| |
| // We take the minimum between tail index and buffer size to make sure we |
| // don't go out of bounds. |
| size_t const max_rect_calls_index = |
| std::min(static_cast<int>(draw_calls_tail_idx_), |
| static_cast<int>(draw_rect_calls_.size())); |
| |
| size_t const max_logs_index = std::min(static_cast<int>(logs_tail_idx_), |
| static_cast<int>(logs_.size())); |
| |
| base::Value::List draw_calls; |
| |
| // Hash set to keep track of threads that have been registered already. |
| base::flat_set<base::PlatformThreadId::UnderlyingType> registered_threads; |
| for (size_t i = 0; i < max_rect_calls_index; ++i) { |
| base::Value::Dict dict = draw_rect_calls_[i].GetDictionaryValue(); |
| dict.Set("size", base::Value::List() |
| .Append(draw_rect_calls_[i].obj_size.width()) |
| .Append(draw_rect_calls_[i].obj_size.height())); |
| dict.Set("pos", |
| base::Value::List() |
| .Append(static_cast<double>(draw_rect_calls_[i].pos.x())) |
| .Append(static_cast<double>(draw_rect_calls_[i].pos.y()))); |
| if (draw_rect_calls_[i].uv != DBG_DEFAULT_UV) { |
| dict.Set( |
| "uv_size", |
| base::Value::List() |
| .Append(static_cast<double>(draw_rect_calls_[i].uv.width())) |
| .Append(static_cast<double>(draw_rect_calls_[i].uv.height()))); |
| dict.Set("uv_pos", |
| base::Value::List() |
| .Append(static_cast<double>(draw_rect_calls_[i].uv.x())) |
| .Append(static_cast<double>(draw_rect_calls_[i].uv.y()))); |
| } |
| dict.Set("buff_id", std::move(draw_rect_calls_[i].buff_id)); |
| if (!draw_rect_calls_[i].text.empty()) { |
| dict.Set("text", std::move(draw_rect_calls_[i].text)); |
| } |
| registered_threads.insert( |
| base::saturated_cast<base::PlatformThreadId::UnderlyingType>( |
| draw_rect_calls_[i].thread_id)); |
| draw_calls.Append(std::move(dict)); |
| } |
| |
| global_dict.Set("drawcalls", std::move(draw_calls)); |
| |
| base::Value::Dict buff_map; |
| |
| for (auto&& each : buffers_) { |
| std::string uri = |
| skia::EncodePngAsDataUri(each.buffer_info.bitmap.pixmap()); |
| if (uri.empty()) { |
| DLOG(ERROR) << "encode failed"; |
| continue; |
| } |
| buff_map.Set(base::NumberToString(each.id), std::move(uri)); |
| } |
| |
| global_dict.Set("buff_map", std::move(buff_map)); |
| |
| base::Value::List logs; |
| for (size_t i = 0; i < max_logs_index; ++i) { |
| base::Value::Dict dict = logs_[i].GetDictionaryValue(); |
| dict.Set("value", std::move(logs_[i].value)); |
| logs.Append(std::move(dict)); |
| registered_threads.insert(logs_[i].thread_id); |
| } |
| global_dict.Set("logs", std::move(logs)); |
| |
| // Gather thread name:id for all active threads this frame. |
| base::Value::List new_threads; |
| for (auto&& thread_id : registered_threads) { |
| std::string cur_thread_name = |
| base::ThreadIdNameManager::GetInstance()->GetName( |
| base::PlatformThreadId(thread_id)); |
| new_threads.Append(base::Value::Dict() |
| .Set("thread_id", static_cast<int32_t>(thread_id)) |
| .Set("thread_name", cur_thread_name)); |
| registered_threads.insert(thread_id); |
| } |
| |
| global_dict.Set("threads", std::move(new_threads)); |
| |
| // Reset index counters for each buffer. |
| buffers_.clear(); |
| draw_calls_tail_idx_ = 0; |
| logs_tail_idx_ = 0; |
| |
| return base::Value(std::move(global_dict)); |
| } |
| |
| void VizDebugger::UpdateFilters() { |
| if (apply_new_filters_next_frame_) { |
| cached_filters_ = new_filters_; |
| for (auto&& source : sources_) { |
| ApplyFilters(source); |
| } |
| new_filters_.clear(); |
| apply_new_filters_next_frame_ = false; |
| } |
| } |
| |
| void VizDebugger::CompleteFrame(uint64_t counter, |
| const gfx::Size& window_pix, |
| base::TimeTicks time_ticks) { |
| if (!enabled_) { |
| return; |
| } |
| read_write_lock_.WriteLock(); |
| UpdateFilters(); |
| json_frame_output_ = FrameAsJson(counter, window_pix, time_ticks); |
| gpu_thread_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&VizDebugger::AddFrame, base::Unretained(this))); |
| read_write_lock_.WriteUnLock(); |
| } |
| |
| void VizDebugger::ApplyFilters(VizDebugger::StaticSource* src) { |
| // In the case of no filters we disable this source. |
| src->active = false; |
| src->enabled = false; |
| // TODO(petermcneeley): We should probably make this string filtering more |
| // optimal. However, for the most part it the cost is only paid on the |
| // application of new filters. |
| auto simple_match = [](const char* source_str, |
| const std::string& filter_match) { |
| if (filter_match.empty() || source_str == nullptr) { |
| return true; |
| } |
| return UNSAFE_TODO(std::strstr(source_str, filter_match.c_str())) != |
| nullptr; |
| }; |
| |
| for (const auto& filter_block : cached_filters_) { |
| if (simple_match(src->file, filter_block.file) && |
| simple_match(src->func, filter_block.func) && |
| simple_match(src->anno, filter_block.anno)) { |
| src->active = filter_block.active; |
| src->enabled = filter_block.enabled; |
| } |
| } |
| } |
| |
| void VizDebugger::RegisterSource(StaticSource* src) { |
| read_write_lock_.WriteLock(); |
| int index = sources_.size(); |
| src->reg_index = index; |
| ApplyFilters(src); |
| sources_.push_back(src); |
| read_write_lock_.WriteUnLock(); |
| } |
| |
| void VizDebugger::Draw(const gfx::SizeF& obj_size, |
| const gfx::Vector2dF& pos, |
| const VizDebugger::StaticSource* dcs, |
| VizDebugger::DrawOption option, |
| int* id, |
| const gfx::RectF& uv, |
| const std::string& text) { |
| DrawInternal(obj_size, pos, dcs, option, id, uv, text); |
| } |
| |
| void VizDebugger::DrawInternal(const gfx::SizeF& obj_size, |
| const gfx::Vector2dF& pos, |
| const VizDebugger::StaticSource* dcs, |
| VizDebugger::DrawOption option, |
| int* id, |
| const gfx::RectF& uv, |
| const std::string& text) { |
| int local_id_buffer = -1; |
| if (id != nullptr) { |
| local_id_buffer = buffer_id++; |
| *id = local_id_buffer; |
| } |
| |
| // Store atomic insertion index in local variable to use to insert into |
| // buffer. |
| int insertion_index; |
| |
| for (;;) { |
| read_write_lock_.ReadLock(); |
| // Get call insertion index. |
| insertion_index = draw_calls_tail_idx_++; |
| // If the insertion index is within bounds, insert call into buffer. |
| if (static_cast<size_t>(insertion_index) < draw_rect_calls_.size()) { |
| int64_t cur_thread_id = |
| static_cast<int64_t>(base::PlatformThread::CurrentId()); |
| draw_rect_calls_[insertion_index] = DrawCall{submission_count_++, |
| dcs->reg_index, |
| cur_thread_id, |
| option, |
| obj_size, |
| pos, |
| local_id_buffer, |
| uv, |
| text}; |
| // Return when call insertion is successful. |
| read_write_lock_.ReadUnlock(); |
| return; |
| } |
| read_write_lock_.ReadUnlock(); |
| // Take write lock to resize and re-adjust buffer tail index after buffer |
| // overflow. |
| read_write_lock_.WriteLock(); |
| // If tail index is over buffer size, then resizing is definitely needed. |
| // Also re-adjust tail index so it's at the start of the new buffer space. |
| if (static_cast<size_t>(draw_calls_tail_idx_) >= draw_rect_calls_.size()) { |
| draw_calls_tail_idx_ = draw_rect_calls_.size(); |
| draw_rect_calls_.resize(draw_rect_calls_.size() * 2); |
| } |
| read_write_lock_.WriteUnLock(); |
| } |
| } |
| |
| void VizDebugger::AddFrame() { |
| // TODO(petermcneeley): This code has duel thread entry. One to launch the |
| // task and one for the task to run. We should improve on this design in the |
| // future and have a better multithreaded frame data aggregation system. |
| read_write_lock_.WriteLock(); |
| DCHECK(gpu_thread_task_runner_->RunsTasksInCurrentSequence()); |
| if (debug_output_.is_bound()) { |
| debug_output_->LogFrame(std::move(json_frame_output_)); |
| } |
| read_write_lock_.WriteUnLock(); |
| } |
| |
| void VizDebugger::FilterDebugStream(base::Value::Dict json) { |
| read_write_lock_.WriteLock(); |
| DCHECK(gpu_thread_task_runner_->RunsTasksInCurrentSequence()); |
| const base::Value::List* filters = json.FindList("filters"); |
| |
| if (!filters) { |
| LOG(ERROR) << "Missing filter list in json: " << json; |
| return; |
| } |
| |
| new_filters_.clear(); |
| |
| for (const auto& entry : *filters) { |
| const auto& filter = entry.GetDict(); |
| const base::Value* file = filter.FindByDottedPath("selector.file"); |
| const base::Value* func = filter.FindByDottedPath("selector.func"); |
| const base::Value* anno = filter.FindByDottedPath("selector.anno"); |
| const base::Value* active = filter.Find("active"); |
| |
| if (!active) { |
| LOG(ERROR) << "Missing filter props in json: " << json; |
| return; |
| } |
| |
| if ((file && !file->is_string()) || (func && !func->is_string()) || |
| (anno && !anno->is_string()) || !active->is_bool()) { |
| LOG(ERROR) << "Filter props wrong type in json: " << json; |
| continue; |
| } |
| |
| auto check_str = [](const base::Value* filter_str) { |
| return (filter_str ? filter_str->GetString() : std::string()); |
| }; |
| |
| std::optional<bool> enabled = filter.FindBool("enabled"); |
| new_filters_.emplace_back(check_str(file), check_str(func), check_str(anno), |
| active->GetBool(), enabled.value_or(true)); |
| } |
| |
| apply_new_filters_next_frame_ = true; |
| read_write_lock_.WriteUnLock(); |
| } |
| |
| void VizDebugger::StartDebugStream( |
| mojo::PendingRemote<mojom::VizDebugOutput> pending_debug_output) { |
| read_write_lock_.WriteLock(); |
| DCHECK(gpu_thread_task_runner_->RunsTasksInCurrentSequence()); |
| debug_output_.Bind(std::move(pending_debug_output)); |
| debug_output_.reset_on_disconnect(); |
| last_sent_source_count_ = 0; |
| |
| // Reset our filters for our new connection. By default the client will send |
| // along the new filters after establishing the connection. |
| new_filters_.clear(); |
| apply_new_filters_next_frame_ = true; |
| |
| debug_output_->LogFrame( |
| base::Value(base::Value::Dict().Set("connection", "ok"))); |
| enabled_.store(true); |
| read_write_lock_.WriteUnLock(); |
| } |
| |
| void VizDebugger::StopDebugStream() { |
| read_write_lock_.WriteLock(); |
| DCHECK(gpu_thread_task_runner_->RunsTasksInCurrentSequence()); |
| debug_output_.reset(); |
| enabled_.store(false); |
| read_write_lock_.WriteUnLock(); |
| } |
| |
| void VizDebugger::AddLogMessage(std::string log, |
| const VizDebugger::StaticSource* dcs, |
| DrawOption option) { |
| // Store atomic insertion index in local variable to use to insert into |
| // buffer. |
| int insertion_index; |
| |
| for (;;) { |
| read_write_lock_.ReadLock(); |
| // Get call insertion index. |
| insertion_index = logs_tail_idx_++; |
| // If the insertion index is within bounds, insert call into buffer. |
| if (static_cast<size_t>(insertion_index) < logs_.size()) { |
| int64_t cur_thread_id = |
| static_cast<int64_t>(base::PlatformThread::CurrentId()); |
| logs_[insertion_index] = LogCall{submission_count_++, dcs->reg_index, |
| cur_thread_id, option, std::move(log)}; |
| // Return when call insertion is successful. |
| read_write_lock_.ReadUnlock(); |
| return; |
| } |
| read_write_lock_.ReadUnlock(); |
| // Take write lock to resize and re-adjust buffer tail index after buffer |
| // overflow. |
| read_write_lock_.WriteLock(); |
| // If tail index is over buffer size, then resizing is definitely needed. |
| // Also re-adjust tail index so it's at the start of the new buffer space. |
| if (static_cast<size_t>(logs_tail_idx_) >= logs_.size()) { |
| logs_tail_idx_ = logs_.size(); |
| logs_.resize(logs_.size() * 2); |
| } |
| read_write_lock_.WriteUnLock(); |
| } |
| } |
| |
| } // namespace viz |
| #else // !VIZ_DEBUGGER_IS_ON() |
| namespace viz { |
| VizDebugger::BufferInfo::BufferInfo() = default; |
| VizDebugger::BufferInfo::~BufferInfo() = default; |
| VizDebugger::BufferInfo::BufferInfo(const BufferInfo& a) = default; |
| |
| std::unique_ptr<base::trace_event::ConvertableToTraceFormat> |
| DrawRectToTraceValue(const gfx::Vector2dF& pos, |
| const gfx::SizeF& size, |
| const std::string& text) { |
| std::unique_ptr<base::trace_event::TracedValue> state( |
| new base::trace_event::TracedValue()); |
| state->SetString("pos_x", base::NumberToString(pos.x())); |
| state->SetString("pos_y", base::NumberToString(pos.y())); |
| state->SetString("size_x", base::NumberToString(size.width())); |
| state->SetString("size_y", base::NumberToString(size.height())); |
| state->SetString("text", text); |
| return state; |
| } |
| |
| } // namespace viz |
| #endif |