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

Unified Diff: tools/win/IdleWakeups/idle_wakeups.cpp

Issue 2356753004: IdleWakeups tool (Closed)
Patch Set: Missing vcproj changes. Created 4 years, 3 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 side-by-side diff with in-line comments
Download patch
Index: tools/win/IdleWakeups/idle_wakeups.cpp
diff --git a/tools/win/IdleWakeups/idle_wakeups.cpp b/tools/win/IdleWakeups/idle_wakeups.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0da1b96bc20312d4ca48e46521f8bf68e72c094a
--- /dev/null
+++ b/tools/win/IdleWakeups/idle_wakeups.cpp
@@ -0,0 +1,629 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "stdafx.h"
+
+#include <map>
+#include <vector>
+#include <algorithm>
+
+#include "power_sampler.h"
+
+// From ntdef.h
+typedef struct _UNICODE_STRING {
+ USHORT Length;
+ USHORT MaximumLength;
+ PWCH Buffer;
+} UNICODE_STRING;
+
+// From <wdm.h>
+typedef LONG KPRIORITY;
+typedef LONG KWAIT_REASON; // Full definition is in wdm.h
+
+// From ntddk.h
+typedef struct _VM_COUNTERS {
+ SIZE_T PeakVirtualSize;
+ SIZE_T VirtualSize;
+ ULONG PageFaultCount;
+ // Padding here in 64-bit
+ SIZE_T PeakWorkingSetSize;
+ SIZE_T WorkingSetSize;
+ SIZE_T QuotaPeakPagedPoolUsage;
+ SIZE_T QuotaPagedPoolUsage;
+ SIZE_T QuotaPeakNonPagedPoolUsage;
+ SIZE_T QuotaNonPagedPoolUsage;
+ SIZE_T PagefileUsage;
+ SIZE_T PeakPagefileUsage;
+} VM_COUNTERS;
+
+// Two possibilities available from here:
+// http://stackoverflow.com/questions/28858849/where-is-system-information-class-defined
+
+typedef enum _SYSTEM_INFORMATION_CLASS {
+ SystemBasicInformation = 0,
+ SystemPerformanceInformation = 2,
+ SystemTimeOfDayInformation = 3,
+ SystemProcessInformation = 5, // This is the number that we need
+ SystemProcessorPerformanceInformation = 8,
+ SystemInterruptInformation = 23,
+ SystemExceptionInformation = 33,
+ SystemRegistryQuotaInformation = 37,
+ SystemLookasideInformation = 45
+} SYSTEM_INFORMATION_CLASS;
+
+// https://msdn.microsoft.com/en-us/library/gg750647.aspx?f=255&MSPPError=-2147217396
+typedef struct {
+ HANDLE UniqueProcess; // Actually process ID
+ HANDLE UniqueThread; // Actually thread ID
+} CLIENT_ID;
+
+// From http://alax.info/blog/1182, with corrections and modifications
+// Originally from
+// http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FSystem%20Information%2FStructures%2FSYSTEM_THREAD.html
+struct SYSTEM_THREAD_INFORMATION {
+ ULONGLONG KernelTime;
+ ULONGLONG UserTime;
+ ULONGLONG CreateTime;
+ ULONG WaitTime;
+ // Padding here in 64-bit
+ PVOID StartAddress;
+ CLIENT_ID ClientId;
+ KPRIORITY Priority;
+ LONG BasePriority;
+ ULONG ContextSwitchCount;
+ ULONG State;
+ KWAIT_REASON WaitReason;
+};
+#if _M_X64
+static_assert(sizeof(SYSTEM_THREAD_INFORMATION) == 80,
+ "Structure size mismatch");
+#else
+static_assert(sizeof(SYSTEM_THREAD_INFORMATION) == 64,
+ "Structure size mismatch");
+#endif
+
+// From http://alax.info/blog/1182, with corrections and modifications
+// Originally from
+// http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FSystem%20Information%2FStructures%2FSYSTEM_THREAD.html
+struct SYSTEM_PROCESS_INFORMATION {
+ ULONG NextEntryOffset;
+ ULONG NumberOfThreads;
+ // 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
+ ULONGLONG WorkingSetPrivateSize;
+ ULONG HardFaultCount;
+ ULONG Reserved1;
+ ULONGLONG CycleTime;
+ ULONGLONG CreateTime;
+ ULONGLONG UserTime;
+ ULONGLONG KernelTime;
+ UNICODE_STRING ImageName;
+ KPRIORITY BasePriority;
+ HANDLE ProcessId;
+ HANDLE ParentProcessId;
+ ULONG HandleCount;
+ ULONG Reserved2[2];
+ // Padding here in 64-bit
+ VM_COUNTERS VirtualMemoryCounters;
+ size_t Reserved3;
+ IO_COUNTERS IoCounters;
+ SYSTEM_THREAD_INFORMATION Threads[1];
+};
+#if _M_X64
+static_assert(sizeof(SYSTEM_PROCESS_INFORMATION) == 336,
+ "Structure size mismatch");
+#else
+static_assert(sizeof(SYSTEM_PROCESS_INFORMATION) == 248,
+ "Structure size mismatch");
+#endif
+
+// ntstatus.h conflicts with windows.h so define this locally.
+#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
+#define STATUS_BUFFER_TOO_SMALL ((NTSTATUS)0xC0000023L)
+#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
+
+typedef NTSTATUS(WINAPI* NTQUERYSYSTEMINFORMATION)(
+ SYSTEM_INFORMATION_CLASS SystemInformationClass,
+ PVOID SystemInformation,
+ ULONG SystemInformationLength,
+ PULONG ReturnLength);
+
+__declspec(noreturn) void oops(const char* pMessage) {
+ printf("%s\n", pMessage);
+ exit(0);
+}
+
+// Contains per thread data stored in each data snapshot.
+struct ThreadData {
+ HANDLE ThreadId;
+ ULONG ContextSwitches;
+};
+
+typedef std::vector<ThreadData> ThreadsVector;
+
+// Contains per process data stored in each data snapshot.
+struct ProcessData {
+ HANDLE ProcessId;
+ ULONGLONG CpuTime;
+ ULONGLONG WorkingSetPrivateSize;
+ ThreadsVector Threads;
+};
+
+// A vector of ProcessData represents one snapshot of perf data
+// collected by the tool each collection interval.
+typedef std::vector<ProcessData> ProcessesVector;
+
+// Result data structure contains a final set of values calculated based on
+// comparison of two snapshots. These are the values that the tool prints
+// in the output.
+struct Result {
+ ULONG IdleWakeupsPerSec;
+ double CpuUsage;
+ ULONGLONG WorkingSet;
+ double Power;
+};
+
+typedef std::vector<Result> ResultVector;
+
+// The following 4 functions are used for sorting of ResultVector.
+ULONG GetIdleWakeupsPerSec(const Result& r) {
+ return r.IdleWakeupsPerSec;
+}
+double GetCpuUsage(const Result& r) {
+ return r.CpuUsage;
+}
+ULONGLONG GetWorkingSet(const Result& r) {
+ return r.WorkingSet;
+}
+double GetPower(const Result& r) {
+ return r.Power;
+}
+
+template <typename T>
+T GetMedian(ResultVector* results, T (*getter)(const Result&)) {
+ std::sort(results->begin(), results->end(),
+ [&](const Result& lhs, const Result& rhs) {
+ return getter(lhs) < getter(rhs);
+ });
+
+ size_t median_index = results->size() / 2;
+ if (results->size() % 2 != 0) {
+ return getter((*results)[median_index]);
+ } else {
+ return (getter((*results)[median_index - 1]) +
+ getter((*results)[median_index])) /
+ 2;
+ }
+}
+
+// This class holds the app state and constains a number of utilities for
+// collecting and diffing snapshots of data, handling processes, etc.
+class IdleWakeups {
+ public:
+ IdleWakeups(const wchar_t* process_name);
+ ~IdleWakeups();
+
+ double TakeSnapshot(ProcessesVector* processes);
+ Result DiffSnapshots(double time_delta,
+ const ProcessesVector& prev_processes,
+ const ProcessesVector& processes);
+
+ void OpenProcesses(const ProcessesVector& processes);
+ void CloseProcesses();
+
+ const wchar_t* target_process_name_filter() const {
+ return target_process_name;
+ }
+
+ private:
+ HANDLE GetProcessHandle(const ProcessData& process);
+ void OpenProcess(const ProcessData& process);
+ void CloseProcess(const ProcessData& process);
+ bool GetFinishedProcessCpuTime(const ProcessData& process,
+ ULONGLONG* cpu_usage);
+
+ static void SortThreads(ThreadsVector* processes);
+ static void SortProcesses(ProcessesVector* processes);
+
+ static ULONG CountContextSwitches(const ProcessData& process_data);
+ static ULONG DiffContextSwitches(const ProcessData& prev_process_data,
+ const ProcessData& process_data);
+
+ DWORD NumberOfprocessors() const { return number_of_processors; }
+
+ NTQUERYSYSTEMINFORMATION nt_query_system_information_ptr;
+ DWORD number_of_processors;
+ wchar_t target_process_name[256];
+ LARGE_INTEGER perf_frequency;
+ LARGE_INTEGER previous_perf_counter_value;
+ ULONG previous_buffer_size = 0;
+
+ // The first argument of HANDLE type is actually used for process IDs. This is
+ // consistent with data structures above.
+ std::map<HANDLE, HANDLE> process_id_to_hanle_map;
+};
+
+IdleWakeups::IdleWakeups(const wchar_t* process_name) {
+ lstrcpyn(target_process_name, process_name,
+ sizeof(target_process_name) / sizeof(wchar_t));
+
+ HMODULE ntdll = GetModuleHandle(_T("ntdll.dll"));
+ if (!ntdll)
+ oops("Couldn't load ntdll.dll");
+ nt_query_system_information_ptr = (NTQUERYSYSTEMINFORMATION)GetProcAddress(
+ ntdll, "NtQuerySystemInformation");
+ if (!nt_query_system_information_ptr)
+ oops("Couldn't find NtQuerySystemInformation");
+
+ SYSTEM_INFO system_info;
+ GetNativeSystemInfo(&system_info);
+ number_of_processors = system_info.dwNumberOfProcessors;
+ printf("Number of processors: %d\n", number_of_processors);
+
+ QueryPerformanceFrequency(&perf_frequency);
+}
+
+IdleWakeups::~IdleWakeups() {
+ CloseProcesses();
+}
+
+void IdleWakeups::OpenProcesses(const ProcessesVector& processes) {
+ for (auto& process : processes) {
+ OpenProcess(process);
+ }
+}
+
+void IdleWakeups::CloseProcesses() {
+ for (auto& pair : process_id_to_hanle_map) {
+ CloseHandle(pair.second);
+ }
+ process_id_to_hanle_map.clear();
+}
+
+HANDLE IdleWakeups::GetProcessHandle(const ProcessData& process_data) {
+ return process_id_to_hanle_map[process_data.ProcessId];
+}
+
+void IdleWakeups::OpenProcess(const ProcessData& process_data) {
+ process_id_to_hanle_map[process_data.ProcessId] =
+ ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE,
+ (DWORD)(ULONGLONG)process_data.ProcessId);
+}
+
+void IdleWakeups::CloseProcess(const ProcessData& process) {
+ HANDLE handle = GetProcessHandle(process);
+ CloseHandle(handle);
+ process_id_to_hanle_map.erase(process.ProcessId);
+}
+
+double IdleWakeups::TakeSnapshot(ProcessesVector* processes) {
+ ULONG data_size;
+
+ LARGE_INTEGER perf_counter_value;
+ std::vector<BYTE> process_data(previous_buffer_size);
+
+ for (;;) {
+ data_size = 0;
+ NTSTATUS result = nt_query_system_information_ptr(
+ SystemProcessInformation,
+ previous_buffer_size > 0 ? &process_data[0] : NULL,
+ previous_buffer_size, &data_size);
+ if (result == STATUS_INFO_LENGTH_MISMATCH ||
+ result == STATUS_BUFFER_TOO_SMALL) {
+ // Reallocate the buffer.
+ previous_buffer_size = data_size;
+ process_data.resize(data_size);
+ continue;
+ }
+
+ if (result != STATUS_SUCCESS)
+ oops("NtQuerySystemInformation failed");
+
+ QueryPerformanceCounter(&perf_counter_value);
+ break;
+ }
+
+ for (size_t offset = 0; offset < data_size;) {
+ auto pi = reinterpret_cast<const SYSTEM_PROCESS_INFORMATION*>(
+ process_data.data() + offset);
+
+ // Validate that the offset is valid and all needed data is within
+ // the buffer boundary.
+ if (offset + sizeof(SYSTEM_PROCESS_INFORMATION) > data_size)
+ break;
+ if (offset + sizeof(SYSTEM_PROCESS_INFORMATION) +
+ (pi->NumberOfThreads - 1) * sizeof(SYSTEM_THREAD_INFORMATION) >
+ data_size)
+ break;
+
+ if (pi->ImageName.Buffer &&
+ wcsncmp(target_process_name_filter(), pi->ImageName.Buffer,
+ lstrlen(target_process_name_filter())) == 0) {
+ // There is no point in recording per-process idle wakeups with any more
+ // precision than they are recorded per-thread because the per-thread
+ // numbers may have wrapped. Therefore idleWakeups can only really be used
+ // to calculate diffs, or for processes that have not run long enough to
+ // accumulate four billion context switches.
+ ProcessData process_data;
+ process_data.ProcessId = pi->ProcessId;
+ process_data.CpuTime = pi->KernelTime + pi->UserTime;
+ process_data.WorkingSetPrivateSize = pi->WorkingSetPrivateSize;
+ for (ULONG thread_index = 0; thread_index < pi->NumberOfThreads;
+ ++thread_index) {
+ const SYSTEM_THREAD_INFORMATION* ti = &pi->Threads[thread_index];
+ if (ti->ClientId.UniqueProcess != pi->ProcessId)
+ continue;
+
+ ThreadData thread_data;
+ thread_data.ThreadId = ti->ClientId.UniqueThread;
+ thread_data.ContextSwitches = ti->ContextSwitchCount;
+ process_data.Threads.push_back(thread_data);
+ }
+
+ SortThreads(&process_data.Threads);
+
+ processes->push_back(process_data);
+ }
+
+ // Check for end of the list.
+ if (!pi->NextEntryOffset)
+ break;
+
+ // Jump to the next entry.
+ offset += pi->NextEntryOffset;
+ }
+
+ SortProcesses(processes);
+
+ double time_delta = 0.0;
+
+ if (previous_perf_counter_value.QuadPart != 0) {
+ time_delta = static_cast<double>(perf_counter_value.QuadPart -
+ previous_perf_counter_value.QuadPart) /
+ perf_frequency.QuadPart;
+ }
+ previous_perf_counter_value = perf_counter_value;
+
+ return time_delta;
+}
+
+void IdleWakeups::SortThreads(ThreadsVector* threads) {
+ std::sort(threads->begin(), threads->end(),
+ [](const ThreadData& lhs, const ThreadData& rhs) {
+ return lhs.ThreadId < rhs.ThreadId;
+ });
+}
+
+void IdleWakeups::SortProcesses(ProcessesVector* processes) {
+ std::sort(processes->begin(), processes->end(),
+ [](const ProcessData& lhs, const ProcessData& rhs) {
+ return lhs.ProcessId < rhs.ProcessId;
+ });
+}
+
+ULONG IdleWakeups::CountContextSwitches(const ProcessData& process_data) {
+ ULONG context_switches = 0;
+
+ for (const auto& thread_data : process_data.Threads) {
+ context_switches += thread_data.ContextSwitches;
+ }
+
+ return context_switches;
+}
+
+ULONG IdleWakeups::DiffContextSwitches(const ProcessData& prev_process_data,
+ const ProcessData& process_data) {
+ ULONG context_switches = 0;
+ size_t prev_index = 0;
+
+ for (const auto& thread_data : process_data.Threads) {
+ ULONG prev_context_switches = 0;
+
+ for (; prev_index < prev_process_data.Threads.size(); ++prev_index) {
+ const auto& prev_thread_data = prev_process_data.Threads[prev_index];
+ if (prev_thread_data.ThreadId == thread_data.ThreadId) {
+ prev_context_switches = prev_thread_data.ContextSwitches;
+ ++prev_index;
+ break;
+ }
+
+ if (prev_thread_data.ThreadId > thread_data.ThreadId)
+ break;
+ }
+
+ context_switches += thread_data.ContextSwitches - prev_context_switches;
+ }
+
+ return context_switches;
+}
+
+bool IdleWakeups::GetFinishedProcessCpuTime(const ProcessData& process,
+ ULONGLONG* cpu_time) {
+ HANDLE process_handle = GetProcessHandle(process);
+
+ FILETIME creation_time, exit_time, kernel_time, user_time;
+ if (GetProcessTimes(process_handle, &creation_time, &exit_time, &kernel_time,
+ &user_time)) {
+ ULARGE_INTEGER ul_kernel_time, ul_user_time;
+ ul_kernel_time.LowPart = kernel_time.dwLowDateTime;
+ ul_kernel_time.HighPart = kernel_time.dwHighDateTime;
+ ul_user_time.LowPart = user_time.dwLowDateTime;
+ ul_user_time.HighPart = user_time.dwHighDateTime;
+ *cpu_time = ul_kernel_time.QuadPart + ul_user_time.QuadPart;
+ return true;
+ }
+
+ *cpu_time = 0;
+ return false;
+}
+
+Result IdleWakeups::DiffSnapshots(double time_delta,
+ const ProcessesVector& prev_processes,
+ const ProcessesVector& processes) {
+ ULONG idle_wakeups_delta = 0;
+ ULONGLONG cpu_usage_delta = 0;
+ ULONGLONG total_working_set = 0;
+
+ size_t prev_index = 0;
+
+ for (const auto& process_data : processes) {
+ const ProcessData* prev_process_data_to_diff = nullptr;
+ ULONGLONG prev_process_cpu_time = 0;
+
+ for (; prev_index < prev_processes.size(); ++prev_index) {
+ const auto& prev_process_data = prev_processes[prev_index];
+
+ if (prev_process_data.ProcessId == process_data.ProcessId) {
+ prev_process_data_to_diff = &prev_process_data;
+ prev_process_cpu_time = prev_process_data.CpuTime;
+ ++prev_index;
+ break;
+ }
+
+ if (prev_process_data.ProcessId > process_data.ProcessId)
+ break;
+
+ // Prev process disappeared.
+ ULONGLONG last_known_cpu_time;
+ if (GetFinishedProcessCpuTime(prev_process_data, &last_known_cpu_time)) {
+ cpu_usage_delta += last_known_cpu_time - prev_process_data.CpuTime;
+ }
+ CloseProcess(prev_process_data);
+ }
+
+ if (prev_process_data_to_diff) {
+ idle_wakeups_delta +=
+ DiffContextSwitches(*prev_process_data_to_diff, process_data);
+ } else {
+ // New process that we haven't seen before.
+ OpenProcess(process_data);
+ idle_wakeups_delta += CountContextSwitches(process_data);
+ }
+
+ cpu_usage_delta += process_data.CpuTime - prev_process_cpu_time;
+ total_working_set += process_data.WorkingSetPrivateSize / 1024;
+ }
+
+ Result result;
+ result.IdleWakeupsPerSec =
+ static_cast<ULONG>(idle_wakeups_delta / time_delta);
+ // brucedawson: Don't divide by number of processors so that all numbers are
+ // percentage of a core
+ // result.CpuUsage = (double)cpu_usage_delta * 100 / (time_delta * 10000000 *
+ // NumberOfprocessors());
+ result.CpuUsage = (double)cpu_usage_delta * 100 / (time_delta * 10000000);
+ result.WorkingSet = total_working_set;
+
+ return result;
+}
+
+HANDLE ctrl_c_pressed = NULL;
+
+BOOL WINAPI HandlerFunction(DWORD ctrl_type) {
+ if (ctrl_type == CTRL_C_EVENT) {
+ printf("Ctrl+C pressed...\n");
+ SetEvent(ctrl_c_pressed);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+const DWORD sleep_time_sec = 2;
+
+void PrintHeader() {
+ printf(
+ "------------------------------------------------------------------------"
+ "----------\n");
+ printf(
+ " Context switches/sec CPU usage Working set "
+ " Power\n");
+ printf(
+ "------------------------------------------------------------------------"
+ "----------\n");
+}
+
+#define RESULT_FORMAT_STRING " %20lu %8.2f%c %6.2f MiB %4.2f W\n"
+
+int wmain(int argc, wchar_t* argv[]) {
+ ctrl_c_pressed = CreateEvent(NULL, FALSE, FALSE, NULL);
+ SetConsoleCtrlHandler(HandlerFunction, TRUE);
+
+ PowerSampler power_sampler;
+
+ IdleWakeups the_app(argc > 1 ? argv[1] : L"chrome.exe");
+
+ ProcessesVector prev_chrome_processes;
+ ProcessesVector chrome_processes;
+
+ // Take the initial snapshot.
+ the_app.TakeSnapshot(&chrome_processes);
+ the_app.OpenProcesses(chrome_processes);
+
+ ULONG cumulative_idle_wakeups_per_sec = 0;
+ double cumulative_cpu_usage = 0.0;
+ ULONGLONG cumulative_working_set = 0;
+ double cumulative_energy = 0.0;
+
+ ResultVector results;
+
+ printf("Capturing perf data for all processes matching %ls\n",
+ the_app.target_process_name_filter());
+
+ PrintHeader();
+
+ for (;;) {
+ prev_chrome_processes.swap(chrome_processes);
+ chrome_processes.clear();
+
+ if (WaitForSingleObject(ctrl_c_pressed, sleep_time_sec * 1000) ==
+ WAIT_OBJECT_0)
+ break;
+
+ double time_delta = the_app.TakeSnapshot(&chrome_processes);
+
+ Result result = the_app.DiffSnapshots(time_delta, prev_chrome_processes,
+ chrome_processes);
+ power_sampler.SampleCPUPowerState();
+ result.Power = power_sampler.get_power(L"Processor");
+
+ printf("%9u processes" RESULT_FORMAT_STRING, (DWORD)chrome_processes.size(),
+ result.IdleWakeupsPerSec, result.CpuUsage, '%',
+ result.WorkingSet / 1024.0, result.Power);
+
+ cumulative_idle_wakeups_per_sec += result.IdleWakeupsPerSec;
+ cumulative_cpu_usage += result.CpuUsage;
+ cumulative_working_set += result.WorkingSet;
+ cumulative_energy += result.Power;
+
+ results.push_back(result);
+ }
+
+ CloseHandle(ctrl_c_pressed);
+
+ ULONG sample_count = (ULONG)results.size();
+ if (sample_count == 0)
+ return 0;
+
+ PrintHeader();
+
+ printf(" Average" RESULT_FORMAT_STRING,
+ cumulative_idle_wakeups_per_sec / sample_count,
+ cumulative_cpu_usage / sample_count, '%',
+ (cumulative_working_set / 1024.0) / sample_count,
+ cumulative_energy / sample_count);
+
+ Result median_result;
+
+ median_result.IdleWakeupsPerSec =
+ GetMedian<ULONG>(&results, GetIdleWakeupsPerSec);
+ median_result.CpuUsage = GetMedian<double>(&results, GetCpuUsage);
+ median_result.WorkingSet = GetMedian<ULONGLONG>(&results, GetWorkingSet);
+ median_result.Power = GetMedian<double>(&results, GetPower);
+
+ printf(" Median" RESULT_FORMAT_STRING,
+ median_result.IdleWakeupsPerSec, median_result.CpuUsage, '%',
+ median_result.WorkingSet / 1024.0, median_result.Power);
+
+ return 0;
+}

Powered by Google App Engine
This is Rietveld 408576698