Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(520)

Side by Side Diff: chrome/browser/task_management/sampling/shared_sampler_win.cc

Issue 2178733002: Task manager should support Idle Wakeups on Windows (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fixed build error on win_clang Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/task_management/sampling/shared_sampler.h"
6
7 #include <windows.h>
8 #include <winternl.h>
9
10 #include <algorithm>
11
12 #include "base/bind.h"
13 #include "base/command_line.h"
14 #include "base/path_service.h"
15 #include "base/time/time.h"
16 #include "chrome/browser/task_management/task_manager_observer.h"
17 #include "chrome/common/chrome_constants.h"
18 #include "content/public/browser/browser_thread.h"
19
20 namespace task_management {
21
22 namespace {
23
24 // From <wdm.h>
25 typedef LONG KPRIORITY;
26 typedef LONG KWAIT_REASON; // Full definition is in wdm.h
27
28 // From ntddk.h
29 typedef struct _VM_COUNTERS {
30 SIZE_T PeakVirtualSize;
31 SIZE_T VirtualSize;
32 ULONG PageFaultCount;
33 // Padding here in 64-bit
34 SIZE_T PeakWorkingSetSize;
35 SIZE_T WorkingSetSize;
36 SIZE_T QuotaPeakPagedPoolUsage;
37 SIZE_T QuotaPagedPoolUsage;
38 SIZE_T QuotaPeakNonPagedPoolUsage;
39 SIZE_T QuotaNonPagedPoolUsage;
40 SIZE_T PagefileUsage;
41 SIZE_T PeakPagefileUsage;
42 } VM_COUNTERS;
43
44 // Two possibilities available from here:
45 // http://stackoverflow.com/questions/28858849/where-is-system-information-class -defined
46
47 typedef enum _SYSTEM_INFORMATION_CLASS {
48 SystemProcessInformation = 5, // This is the number that we need.
49 } SYSTEM_INFORMATION_CLASS;
50
51 // https://msdn.microsoft.com/en-us/library/gg750647.aspx?f=255&MSPPError=-21472 17396
52 typedef struct {
53 HANDLE UniqueProcess; // Actually process ID
54 HANDLE UniqueThread; // Actually thread ID
55 } CLIENT_ID;
56
57 // From http://alax.info/blog/1182, with corrections and modifications
58 // Originally from
59 // http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%2 0Functions%2FSystem%20Information%2FStructures%2FSYSTEM_THREAD.html
60 struct SYSTEM_THREAD_INFORMATION {
61 ULONGLONG KernelTime;
62 ULONGLONG UserTime;
63 ULONGLONG CreateTime;
64 ULONG WaitTime;
65 // Padding here in 64-bit
66 PVOID StartAddress;
67 CLIENT_ID ClientId;
68 KPRIORITY Priority;
69 LONG BasePriority;
70 ULONG ContextSwitchCount;
71 ULONG State;
72 KWAIT_REASON WaitReason;
73 };
74 #if _M_X64
75 static_assert(sizeof(SYSTEM_THREAD_INFORMATION) == 80,
76 "Structure size mismatch");
77 #else
78 static_assert(sizeof(SYSTEM_THREAD_INFORMATION) == 64,
79 "Structure size mismatch");
80 #endif
81
82 // From http://alax.info/blog/1182, with corrections and modifications
83 // Originally from
84 // http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%2 0Functions%2FSystem%20Information%2FStructures%2FSYSTEM_THREAD.html
85 struct SYSTEM_PROCESS_INFORMATION {
86 ULONG NextEntryOffset;
87 ULONG NumberOfThreads;
88 // http://processhacker.sourceforge.net/doc/struct___s_y_s_t_e_m___p_r_o_c_e_s _s___i_n_f_o_r_m_a_t_i_o_n.html
89 ULONGLONG WorkingSetPrivateSize;
90 ULONG HardFaultCount;
91 ULONG Reserved1;
92 ULONGLONG CycleTime;
93 ULONGLONG CreateTime;
94 ULONGLONG UserTime;
95 ULONGLONG KernelTime;
96 UNICODE_STRING ImageName;
97 KPRIORITY BasePriority;
98 HANDLE ProcessId;
99 HANDLE ParentProcessId;
100 ULONG HandleCount;
101 ULONG Reserved2[2];
102 // Padding here in 64-bit
103 VM_COUNTERS VirtualMemoryCounters;
104 size_t Reserved3;
105 IO_COUNTERS IoCounters;
106 SYSTEM_THREAD_INFORMATION Threads[1];
107 };
108 #if _M_X64
109 static_assert(sizeof(SYSTEM_PROCESS_INFORMATION) == 336,
110 "Structure size mismatch");
111 #else
112 static_assert(sizeof(SYSTEM_PROCESS_INFORMATION) == 248,
113 "Structure size mismatch");
114 #endif
115
116 // ntstatus.h conflicts with windows.h so define this locally.
117 #define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
118 #define STATUS_BUFFER_TOO_SMALL ((NTSTATUS)0xC0000023L)
119 #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
120
121 // Simple memory buffer wrapper for passing the data out of
122 // QuerySystemProcessInformation.
123 class ByteBuffer {
124 public:
125 explicit ByteBuffer(size_t capacity)
126 : size_(0), capacity_(0) {
127 if (capacity > 0)
128 grow(capacity);
129 }
130
131 ~ByteBuffer() {}
132
133 BYTE* data() { return data_.get(); }
134
135 size_t size() { return size_; }
136
137 void set_size(size_t new_size) {
138 DCHECK_LE(new_size, capacity_);
139 size_ = new_size;
140 }
141
142 size_t capacity() { return capacity_; }
143
144 void grow(size_t new_capacity) {
145 DCHECK_GT(new_capacity, capacity_);
146 capacity_ = new_capacity;
147 data_.reset(new BYTE[new_capacity]);
148 }
149
150 private:
151 std::unique_ptr<BYTE[]> data_;
152 size_t size_;
153 size_t capacity_;
154
155 DISALLOW_COPY_AND_ASSIGN(ByteBuffer);
156 };
157
158 // Wrapper for NtQuerySystemProcessInformation with buffer reallocation logic.
159 bool QuerySystemProcessInformation(ByteBuffer* buffer) {
160 typedef NTSTATUS(WINAPI * NTQUERYSYSTEMINFORMATION)(
161 SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation,
162 ULONG SystemInformationLength, PULONG ReturnLength);
163
164 HMODULE ntdll = ::GetModuleHandle(L"ntdll.dll");
165 if (!ntdll) {
166 NOTREACHED();
167 return false;
168 }
169
170 NTQUERYSYSTEMINFORMATION nt_query_system_information_ptr =
171 reinterpret_cast<NTQUERYSYSTEMINFORMATION>(
172 ::GetProcAddress(ntdll, "NtQuerySystemInformation"));
173 if (!nt_query_system_information_ptr) {
174 NOTREACHED();
175 return false;
176 }
177
178 NTSTATUS result;
179
180 // There is a potential race condition between growing the buffer and new
181 // processes being created. Try a few times before giving up.
182 for (int i = 0; i < 10; i++) {
183 ULONG data_size = 0;
184 ULONG buffer_size = static_cast<ULONG>(buffer->capacity());
185 result = nt_query_system_information_ptr(
186 SystemProcessInformation,
187 buffer->data(), buffer_size, &data_size);
188
189 if (result == STATUS_SUCCESS) {
190 buffer->set_size(data_size);
191 break;
192 }
193
194 if (result == STATUS_INFO_LENGTH_MISMATCH ||
195 result == STATUS_BUFFER_TOO_SMALL) {
196 // Insufficient buffer. Grow to the returned |data_size| plus 10% extra
197 // to avoid frequent reallocations and try again.
198 DCHECK_GT(data_size, buffer_size);
199 buffer->grow(static_cast<ULONG>(data_size * 1.1));
200 } else {
201 // An error other than the two above.
202 break;
203 }
204 }
205
206 return result == STATUS_SUCCESS;
207 }
208
209 // Per-thread data extracted from SYSTEM_THREAD_INFORMATION and stored in a
210 // snapshot. This structure is accessed only on the worker thread.
211 struct ThreadData {
212 base::PlatformThreadId thread_id;
213 ULONG context_switches;
214 };
215
216 // Per-process data extracted from SYSTEM_PROCESS_INFORMATION and stored in a
217 // snapshot. This structure is accessed only on the worker thread.
218 struct ProcessData {
219 ProcessData() = default;
220 ProcessData(ProcessData&&) = default;
221
222 std::vector<ThreadData> threads;
223
224 private:
225 DISALLOW_COPY_AND_ASSIGN(ProcessData);
226 };
227
228 typedef std::map<base::ProcessId, ProcessData> ProcessDataMap;
229
230 ULONG CountContextSwitchesDelta(const ProcessData& prev_process_data,
231 const ProcessData& new_process_data) {
232 // This one pass algorithm relies on the threads vectors to be
233 // ordered by thread_id.
234 ULONG delta = 0;
235 size_t prev_index = 0;
236
237 for (const auto& new_thread : new_process_data.threads) {
238 ULONG prev_thread_context_switches = 0;
239
240 // Iterate over the process threads from the previous snapshot skipping
241 // threads that don't exist anymore. Please note that this iteration starts
242 // from the last known prev_index and goes until a previous snapshot's
243 // thread ID >= the current snapshot's thread ID. So the overall algorithm
244 // is linear.
245 for (; prev_index < prev_process_data.threads.size(); ++prev_index) {
246 const auto& prev_thread = prev_process_data.threads[prev_index];
247 if (prev_thread.thread_id == new_thread.thread_id) {
248 // Threads match between two snapshots. Use the previous snapshot
249 // thread's context_switches to subtract from the delta.
250 prev_thread_context_switches = prev_thread.context_switches;
251 ++prev_index;
252 break;
253 }
254
255 if (prev_thread.thread_id > new_thread.thread_id) {
256 // This is due to a new thread that didn't exist in the previous
257 // snapshot. Keep the zero value of |prev_thread_context_switches| which
258 // essentially means the entire number of context switches of the new
259 // thread will be added to the delta.
260 break;
261 }
262 }
263
264 delta += new_thread.context_switches - prev_thread_context_switches;
265 }
266
267 return delta;
268 }
269
270 // Seeks a matching ProcessData by Process ID in a previous snapshot.
271 // This uses the fact that ProcessDataMap entries are ordered by Process ID.
272 const ProcessData* SeekInPreviousSnapshot(
273 base::ProcessId process_id, ProcessDataMap::const_iterator* iter_to_advance,
274 const ProcessDataMap::const_iterator& range_end) {
275 for (; *iter_to_advance != range_end; ++(*iter_to_advance)) {
276 if ((*iter_to_advance)->first == process_id) {
277 return &((*iter_to_advance)++)->second;
278 }
279 if ((*iter_to_advance)->first > process_id)
280 break;
281 }
282
283 return nullptr;
284 }
285
286 } // namespace
287
288 // ProcessDataSnapshot gets created and accessed only on the worker thread.
289 // This is used to calculate metrics like Idle Wakeups / sec that require
290 // a delta between two snapshots.
291 // Please note that ProcessDataSnapshot has to be outside of anonymous namespace
292 // in order to match the declaration in shared_sampler.h.
293 struct ProcessDataSnapshot {
294 ProcessDataMap processes;
295 base::TimeTicks timestamp;
296 };
297
298 SharedSampler::SharedSampler(
299 const scoped_refptr<base::SequencedTaskRunner>& blocking_pool_runner)
300 : refresh_flags_(0), previous_buffer_size_(0),
301 supported_image_names_(GetSupportedImageNames()),
302 blocking_pool_runner_(blocking_pool_runner) {
303 DCHECK(blocking_pool_runner.get());
304
305 // This object will be created on the UI thread, however the sequenced checker
306 // will be used to assert we're running the expensive operations on one of the
307 // blocking pool threads.
308 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
309 worker_pool_sequenced_checker_.DetachFromSequence();
310 }
311
312 SharedSampler::~SharedSampler() {}
313
314 int64_t SharedSampler::GetSupportedFlags() const {
315 return REFRESH_TYPE_IDLE_WAKEUPS;
316 }
317
318 SharedSampler::Callbacks::Callbacks() {}
319
320 SharedSampler::Callbacks::~Callbacks() {}
321
322 SharedSampler::Callbacks::Callbacks(Callbacks&& other) {
323 on_idle_wakeups = std::move(other.on_idle_wakeups);
324 }
325
326 void SharedSampler::RegisterCallbacks(
327 base::ProcessId process_id,
328 const OnIdleWakeupsCallback& on_idle_wakeups) {
329 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
330
331 if (process_id == 0)
332 return;
333
334 Callbacks callbacks;
335 callbacks.on_idle_wakeups = on_idle_wakeups;
336 bool result = callbacks_map_.insert(
337 std::make_pair(process_id, std::move(callbacks))).second;
338 DCHECK(result);
339 }
340
341 void SharedSampler::UnregisterCallbacks(base::ProcessId process_id) {
342 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
343
344 if (process_id == 0)
345 return;
346
347 callbacks_map_.erase(process_id);
348
349 if (callbacks_map_.empty())
350 ClearState();
351 }
352
353 void SharedSampler::Refresh(base::ProcessId process_id, int64_t refresh_flags) {
354 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
355 DCHECK(callbacks_map_.find(process_id) != callbacks_map_.end());
356 DCHECK_NE(0, refresh_flags & GetSupportedFlags());
357
358 if (process_id == 0)
359 return;
360
361 if (refresh_flags_ == 0) {
362 base::PostTaskAndReplyWithResult(
363 blocking_pool_runner_.get(), FROM_HERE,
364 base::Bind(&SharedSampler::RefreshOnWorkerThread, this),
365 base::Bind(&SharedSampler::OnRefreshDone, this));
366 } else {
367 // A group of consecutive Refresh calls should all specify the same refresh
368 // flags.
369 DCHECK_EQ(refresh_flags, refresh_flags_);
370 }
371
372 refresh_flags_ |= refresh_flags;
373 }
374
375 void SharedSampler::ClearState() {
376 previous_snapshot_.reset();
377 }
378
379 std::unique_ptr<SharedSampler::RefreshResults>
380 SharedSampler::RefreshOnWorkerThread() {
381 DCHECK(worker_pool_sequenced_checker_.CalledOnValidSequence());
382
383 std::unique_ptr<RefreshResults> results(new RefreshResults);
384
385 std::unique_ptr<ProcessDataSnapshot> snapshot = CaptureSnapshot();
386 if (snapshot) {
387 if (previous_snapshot_) {
388 MakeResultsFromTwoSnapshots(
389 *previous_snapshot_, *snapshot, results.get());
390 } else {
391 MakeResultsFromSnapshot(*snapshot, results.get());
392 }
393
394 previous_snapshot_ = std::move(snapshot);
395 } else {
396 // Failed to get snapshot. This is unlikely.
397 ClearState();
398 }
399
400 return results;
401 }
402
403 /* static */
404 std::vector<base::FilePath> SharedSampler::GetSupportedImageNames() {
405 const wchar_t kNacl64Exe[] = L"nacl64.exe";
406
407 std::vector<base::FilePath> supported_names;
408
409 base::FilePath current_exe;
410 if (PathService::Get(base::FILE_EXE, &current_exe))
411 supported_names.push_back(current_exe.BaseName());
412
413 supported_names.push_back(
414 base::FilePath(chrome::kBrowserProcessExecutableName));
415 supported_names.push_back(base::FilePath(kNacl64Exe));
416
417 return supported_names;
418 }
419
420 bool SharedSampler::IsSupportedImageName(
421 base::FilePath::StringPieceType image_name) const {
422 for (const base::FilePath supported_name : supported_image_names_) {
423 if (base::FilePath::CompareEqualIgnoreCase(image_name,
424 supported_name.value()))
425 return true;
426 }
427
428 return false;
429 }
430
431 std::unique_ptr<ProcessDataSnapshot> SharedSampler::CaptureSnapshot() {
432 DCHECK(worker_pool_sequenced_checker_.CalledOnValidSequence());
433
434 // Preallocate the buffer with the size determined on the previous call to
435 // QuerySystemProcessInformation. This should be sufficient most of the time.
436 // QuerySystemProcessInformation will grow the buffer if necessary.
437 ByteBuffer data_buffer(previous_buffer_size_);
438
439 if (!QuerySystemProcessInformation(&data_buffer))
440 return std::unique_ptr<ProcessDataSnapshot>();
441
442 previous_buffer_size_ = data_buffer.capacity();
443
444 std::unique_ptr<ProcessDataSnapshot> snapshot(new ProcessDataSnapshot);
445 snapshot->timestamp = base::TimeTicks::Now();
446
447 for (size_t offset = 0; offset < data_buffer.size(); ) {
448 auto pi = reinterpret_cast<const SYSTEM_PROCESS_INFORMATION*>(
449 data_buffer.data() + offset);
450
451 // Validate that the offset is valid and all needed data is within
452 // the buffer boundary.
453 if (offset + sizeof(SYSTEM_PROCESS_INFORMATION) > data_buffer.size())
454 break;
455 if (offset + sizeof(SYSTEM_PROCESS_INFORMATION) +
456 (pi->NumberOfThreads - 1) * sizeof(SYSTEM_THREAD_INFORMATION) >
457 data_buffer.size())
458 break;
459
460 if (pi->ImageName.Buffer) {
461 // Validate that the image name is within the buffer boundary.
462 // ImageName.Length seems to be in bytes rather than characters.
463 ULONG image_name_offset =
464 reinterpret_cast<BYTE*>(pi->ImageName.Buffer) - data_buffer.data();
465 if (image_name_offset + pi->ImageName.Length > data_buffer.size())
466 break;
467
468 // Check if this is a chrome process. Ignore all other processes.
469 if (IsSupportedImageName(pi->ImageName.Buffer)) {
470 // Collect enough data to be able to do a diff between two snapshots.
471 // Some threads might stop or new threads might be created between two
472 // snapshots. If a thread with a large number of context switches gets
473 // terminated the total number of context switches for the process might
474 // go down and the delta would be negative.
475 // To avoid that we need to compare thread IDs between two snapshots and
476 // not count context switches for threads that are missing in the most
477 // recent snapshot.
478 ProcessData process_data;
479
480 // Iterate over threads and store each thread's ID and number of context
481 // switches.
482 for (ULONG thread_index = 0; thread_index < pi->NumberOfThreads;
483 ++thread_index) {
484 const SYSTEM_THREAD_INFORMATION* ti = &pi->Threads[thread_index];
485 if (ti->ClientId.UniqueProcess != pi->ProcessId)
486 continue;
487
488 ThreadData thread_data;
489 thread_data.thread_id = static_cast<base::PlatformThreadId>(
490 reinterpret_cast<uintptr_t>(ti->ClientId.UniqueThread));
491 thread_data.context_switches = ti->ContextSwitchCount;
492 process_data.threads.push_back(thread_data);
493 }
494
495 // Order thread data by thread ID to help diff two snapshots.
496 std::sort(process_data.threads.begin(), process_data.threads.end(),
497 [](const ThreadData& l, const ThreadData r) {
498 return l.thread_id < r.thread_id;
499 });
500
501 base::ProcessId process_id = static_cast<base::ProcessId>(
502 reinterpret_cast<uintptr_t>(pi->ProcessId));
503 bool inserted = snapshot->processes.insert(
504 std::make_pair(process_id, std::move(process_data))).second;
505 DCHECK(inserted);
506 }
507 }
508
509 // Check for end of the list.
510 if (!pi->NextEntryOffset)
511 break;
512
513 // Jump to the next entry.
514 offset += pi->NextEntryOffset;
515 }
516
517 return snapshot;
518 }
519
520 void SharedSampler::MakeResultsFromTwoSnapshots(
521 const ProcessDataSnapshot& prev_snapshot,
522 const ProcessDataSnapshot& snapshot,
523 RefreshResults* results) {
524 // Time delta in seconds.
525 double time_delta = (snapshot.timestamp - prev_snapshot.timestamp)
526 .InSecondsF();
527
528 // Iterate over processes in both snapshots in parallel. This algorithm relies
529 // on map entries being ordered by Process ID.
530 ProcessDataMap::const_iterator prev_iter = prev_snapshot.processes.begin();
531
532 for (const auto& current_entry : snapshot.processes) {
533 base::ProcessId process_id = current_entry.first;
534 const ProcessData& process = current_entry.second;
535
536 const ProcessData* prev_snapshot_process = SeekInPreviousSnapshot(
537 process_id, &prev_iter, prev_snapshot.processes.end());
538
539 // Delta between the old snapshot and the new snapshot.
540 int idle_wakeups_delta;
541
542 if (prev_snapshot_process) {
543 // Processes match between two snapshots. Diff context switches.
544 idle_wakeups_delta =
545 CountContextSwitchesDelta(*prev_snapshot_process, process);
546 } else {
547 // Process is missing in the previous snapshot.
548 // Use entire number of context switches of the current process.
549 idle_wakeups_delta = CountContextSwitchesDelta(ProcessData(), process);
550 }
551
552 RefreshResult result;
553 result.process_id = process_id;
554 result.idle_wakeups_per_second =
555 static_cast<int>(round(idle_wakeups_delta / time_delta));
556 results->push_back(result);
557 }
558 }
559
560 void SharedSampler::MakeResultsFromSnapshot(const ProcessDataSnapshot& snapshot,
561 RefreshResults* results) {
562 for (const auto& pair : snapshot.processes) {
563 RefreshResult result;
564 result.process_id = pair.first;
565 // Use 0 for Idle Wakeups / sec in this case. This is consistent with
566 // ProcessMetrics::CalculateIdleWakeupsPerSecond implementation.
567 result.idle_wakeups_per_second = 0;
568 results->push_back(result);
569 }
570 }
571
572 void SharedSampler::OnRefreshDone(
573 std::unique_ptr<RefreshResults> refresh_results) {
574 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
575 DCHECK_NE(0, refresh_flags_);
576
577 size_t result_index = 0;
578
579 for (const auto& callback_entry : callbacks_map_) {
580 base::ProcessId process_id = callback_entry.first;
581 // A sentinel value of -1 is used when the result isn't available.
582 // Task manager will use this to display 'N/A'.
583 int idle_wakeups_per_second = -1;
584
585 // Match refresh result by |process_id|.
586 // This relies on refresh results being ordered by Process ID.
587 // Please note that |refresh_results| might contain some extra entries that
588 // don't exist in |callbacks_map_| if there is more than one instance of
589 // Chrome. It might be missing some entries too if there is a race condition
590 // between getting process information on the worker thread and adding a
591 // corresponding TaskGroup to the task manager.
592 for (; result_index < refresh_results->size(); ++result_index) {
593 const auto& result = (*refresh_results)[result_index];
594 if (result.process_id == process_id) {
595 // Data matched in |refresh_results|.
596 idle_wakeups_per_second = result.idle_wakeups_per_second;
597 ++result_index;
598 break;
599 }
600
601 if (result.process_id > process_id) {
602 // An entry corresponding to |process_id| is missing. See above.
603 break;
604 }
605 }
606
607 if (TaskManagerObserver::IsResourceRefreshEnabled(REFRESH_TYPE_IDLE_WAKEUPS,
608 refresh_flags_)) {
609 callback_entry.second.on_idle_wakeups.Run(idle_wakeups_per_second);
610 }
611 }
612
613 // Reset refresh_results_ to trigger RefreshOnWorkerThread next time Refresh
614 // is called.
615 refresh_flags_ = 0;
616 }
617
618 } // namespace task_management
OLDNEW
« no previous file with comments | « chrome/browser/task_management/sampling/shared_sampler_posix.cc ('k') | chrome/browser/task_management/sampling/task_group.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698