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

Side by Side Diff: base/message_loop/message_pump_win.cc

Issue 2488843002: Switch MessagePumpWin to use base::win::MessageWindow (Closed)
Patch Set: Created 4 years, 1 month 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
« no previous file with comments | « base/message_loop/message_pump_win.h ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "base/message_loop/message_pump_win.h" 5 #include "base/message_loop/message_pump_win.h"
6 6
7 #include <math.h> 7 #include <math.h>
8 #include <stdint.h> 8 #include <stdint.h>
9 9
10 #include <limits> 10 #include <limits>
(...skipping 13 matching lines...) Expand all
24 enum MessageLoopProblems { 24 enum MessageLoopProblems {
25 MESSAGE_POST_ERROR, 25 MESSAGE_POST_ERROR,
26 COMPLETION_POST_ERROR, 26 COMPLETION_POST_ERROR,
27 SET_TIMER_ERROR, 27 SET_TIMER_ERROR,
28 RECEIVED_WM_QUIT_ERROR, 28 RECEIVED_WM_QUIT_ERROR,
29 MESSAGE_LOOP_PROBLEM_MAX, 29 MESSAGE_LOOP_PROBLEM_MAX,
30 }; 30 };
31 31
32 } // namespace 32 } // namespace
33 33
34 static const wchar_t kWndClassFormat[] = L"Chrome_MessagePumpWindow_%p";
35
36 // Message sent to get an additional time slice for pumping (processing) another 34 // Message sent to get an additional time slice for pumping (processing) another
37 // task (a series of such messages creates a continuous task pump). 35 // task (a series of such messages creates a continuous task pump).
38 static const int kMsgHaveWork = WM_USER + 1; 36 static const int kMsgHaveWork = WM_USER + 1;
39 37
40 // The application-defined code passed to the hook procedure. 38 // The application-defined code passed to the hook procedure.
41 static const int kMessageFilterCode = 0x5001; 39 static const int kMessageFilterCode = 0x5001;
42 40
43 //----------------------------------------------------------------------------- 41 //-----------------------------------------------------------------------------
44 // MessagePumpWin public: 42 // MessagePumpWin public:
45 43
46 MessagePumpWin::MessagePumpWin() { 44 MessagePumpWin::MessagePumpWin() = default;
47 }
48 45
49 void MessagePumpWin::Run(Delegate* delegate) { 46 void MessagePumpWin::Run(Delegate* delegate) {
50 RunState s; 47 RunState s;
51 s.delegate = delegate; 48 s.delegate = delegate;
52 s.should_quit = false; 49 s.should_quit = false;
53 s.run_depth = state_ ? state_->run_depth + 1 : 1; 50 s.run_depth = state_ ? state_->run_depth + 1 : 1;
54 51
55 // TODO(stanisc): crbug.com/596190: Remove this code once the bug is fixed. 52 // TODO(stanisc): crbug.com/596190: Remove this code once the bug is fixed.
56 s.schedule_work_error_count = 0; 53 s.schedule_work_error_count = 0;
57 s.last_schedule_work_error_time = Time(); 54 s.last_schedule_work_error_time = Time();
(...skipping 29 matching lines...) Expand all
87 // "overflowingly" large, that means a delayed task was posted with a 84 // "overflowingly" large, that means a delayed task was posted with a
88 // super-long delay. 85 // super-long delay.
89 return timeout < 0 ? 0 : 86 return timeout < 0 ? 0 :
90 (timeout > std::numeric_limits<int>::max() ? 87 (timeout > std::numeric_limits<int>::max() ?
91 std::numeric_limits<int>::max() : static_cast<int>(timeout)); 88 std::numeric_limits<int>::max() : static_cast<int>(timeout));
92 } 89 }
93 90
94 //----------------------------------------------------------------------------- 91 //-----------------------------------------------------------------------------
95 // MessagePumpForUI public: 92 // MessagePumpForUI public:
96 93
97 MessagePumpForUI::MessagePumpForUI() 94 MessagePumpForUI::MessagePumpForUI() {
98 : atom_(0) { 95 DCHECK(message_window_.Create(Bind(&MessagePumpForUI::MessageCallback,
dcheng 2016/11/08 23:20:39 I think this can't be in a DCHECK =)
robliao 2016/11/08 23:31:56 This migrates the previous behavior: message_hwnd_
scottmg 2016/11/08 23:33:54 DCHECKs are compiled out, with side-effects lost i
robliao 2016/11/08 23:47:38 Doh! Indeed. Fixed. Strange that this passed the b
scottmg 2016/11/08 23:50:26 Unfortunately the tests on trybots are only releas
robliao 2016/11/09 00:54:01 Duly noted! Thanks! For trivia, this line expands
scottmg 2016/11/09 01:00:01 Yeah, that sucks. :( I have the feeling that it's
99 InitMessageWnd(); 96 Unretained(this))));
100 } 97 }
101 98
102 MessagePumpForUI::~MessagePumpForUI() { 99 MessagePumpForUI::~MessagePumpForUI() = default;
103 DestroyWindow(message_hwnd_);
104 UnregisterClass(MAKEINTATOM(atom_), CURRENT_MODULE());
105 }
106 100
107 void MessagePumpForUI::ScheduleWork() { 101 void MessagePumpForUI::ScheduleWork() {
108 if (InterlockedExchange(&work_state_, HAVE_WORK) != READY) 102 if (InterlockedExchange(&work_state_, HAVE_WORK) != READY)
109 return; // Someone else continued the pumping. 103 return; // Someone else continued the pumping.
110 104
111 // Make sure the MessagePump does some work for us. 105 // Make sure the MessagePump does some work for us.
112 BOOL ret = PostMessage(message_hwnd_, kMsgHaveWork, 106 BOOL ret = PostMessage(message_window_.hwnd(), kMsgHaveWork, 0, 0);
113 reinterpret_cast<WPARAM>(this), 0);
114 if (ret) 107 if (ret)
115 return; // There was room in the Window Message queue. 108 return; // There was room in the Window Message queue.
116 109
117 // We have failed to insert a have-work message, so there is a chance that we 110 // We have failed to insert a have-work message, so there is a chance that we
118 // will starve tasks/timers while sitting in a nested message loop. Nested 111 // will starve tasks/timers while sitting in a nested message loop. Nested
119 // loops only look at Windows Message queues, and don't look at *our* task 112 // loops only look at Windows Message queues, and don't look at *our* task
120 // queues, etc., so we might not get a time slice in such. :-( 113 // queues, etc., so we might not get a time slice in such. :-(
121 // We could abort here, but the fear is that this failure mode is plausibly 114 // We could abort here, but the fear is that this failure mode is plausibly
122 // common (queue is full, of about 2000 messages), so we'll do a near-graceful 115 // common (queue is full, of about 2000 messages), so we'll do a near-graceful
123 // recovery. Nested loops are pretty transient (we think), so this will 116 // recovery. Nested loops are pretty transient (we think), so this will
124 // probably be recoverable. 117 // probably be recoverable.
125 118
126 // Clarify that we didn't really insert. 119 // Clarify that we didn't really insert.
127 InterlockedExchange(&work_state_, READY); 120 InterlockedExchange(&work_state_, READY);
128 UMA_HISTOGRAM_ENUMERATION("Chrome.MessageLoopProblem", MESSAGE_POST_ERROR, 121 UMA_HISTOGRAM_ENUMERATION("Chrome.MessageLoopProblem", MESSAGE_POST_ERROR,
129 MESSAGE_LOOP_PROBLEM_MAX); 122 MESSAGE_LOOP_PROBLEM_MAX);
130 state_->schedule_work_error_count++; 123 state_->schedule_work_error_count++;
131 state_->last_schedule_work_error_time = Time::Now(); 124 state_->last_schedule_work_error_time = Time::Now();
132 } 125 }
133 126
134 void MessagePumpForUI::ScheduleDelayedWork(const TimeTicks& delayed_work_time) { 127 void MessagePumpForUI::ScheduleDelayedWork(const TimeTicks& delayed_work_time) {
135 delayed_work_time_ = delayed_work_time; 128 delayed_work_time_ = delayed_work_time;
136 RescheduleTimer(); 129 RescheduleTimer();
137 } 130 }
138 131
139 //----------------------------------------------------------------------------- 132 //-----------------------------------------------------------------------------
140 // MessagePumpForUI private: 133 // MessagePumpForUI private:
141 134
142 // static 135 bool MessagePumpForUI::MessageCallback(
143 LRESULT CALLBACK MessagePumpForUI::WndProcThunk( 136 UINT message, WPARAM wparam, LPARAM lparam, LRESULT* result) {
144 HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
145 switch (message) { 137 switch (message) {
146 case kMsgHaveWork: 138 case kMsgHaveWork:
147 reinterpret_cast<MessagePumpForUI*>(wparam)->HandleWorkMessage(); 139 HandleWorkMessage();
148 break; 140 break;
149 case WM_TIMER: 141 case WM_TIMER:
150 reinterpret_cast<MessagePumpForUI*>(wparam)->HandleTimerMessage(); 142 HandleTimerMessage();
151 break; 143 break;
152 } 144 }
153 return DefWindowProc(hwnd, message, wparam, lparam); 145 return false;
154 } 146 }
155 147
156 void MessagePumpForUI::DoRunLoop() { 148 void MessagePumpForUI::DoRunLoop() {
157 // IF this was just a simple PeekMessage() loop (servicing all possible work 149 // IF this was just a simple PeekMessage() loop (servicing all possible work
158 // queues), then Windows would try to achieve the following order according 150 // queues), then Windows would try to achieve the following order according
159 // to MSDN documentation about PeekMessage with no filter): 151 // to MSDN documentation about PeekMessage with no filter):
160 // * Sent messages 152 // * Sent messages
161 // * Posted messages 153 // * Posted messages
162 // * Sent messages (again) 154 // * Sent messages (again)
163 // * WM_PAINT messages 155 // * WM_PAINT messages
(...skipping 20 matching lines...) Expand all
184 if (state_->should_quit) 176 if (state_->should_quit)
185 break; 177 break;
186 178
187 more_work_is_plausible |= 179 more_work_is_plausible |=
188 state_->delegate->DoDelayedWork(&delayed_work_time_); 180 state_->delegate->DoDelayedWork(&delayed_work_time_);
189 // If we did not process any delayed work, then we can assume that our 181 // If we did not process any delayed work, then we can assume that our
190 // existing WM_TIMER if any will fire when delayed work should run. We 182 // existing WM_TIMER if any will fire when delayed work should run. We
191 // don't want to disturb that timer if it is already in flight. However, 183 // don't want to disturb that timer if it is already in flight. However,
192 // if we did do all remaining delayed work, then lets kill the WM_TIMER. 184 // if we did do all remaining delayed work, then lets kill the WM_TIMER.
193 if (more_work_is_plausible && delayed_work_time_.is_null()) 185 if (more_work_is_plausible && delayed_work_time_.is_null())
194 KillTimer(message_hwnd_, reinterpret_cast<UINT_PTR>(this)); 186 KillTimer(message_window_.hwnd(), reinterpret_cast<UINT_PTR>(this));
195 if (state_->should_quit) 187 if (state_->should_quit)
196 break; 188 break;
197 189
198 if (more_work_is_plausible) 190 if (more_work_is_plausible)
199 continue; 191 continue;
200 192
201 more_work_is_plausible = state_->delegate->DoIdleWork(); 193 more_work_is_plausible = state_->delegate->DoIdleWork();
202 if (state_->should_quit) 194 if (state_->should_quit)
203 break; 195 break;
204 196
205 if (more_work_is_plausible) 197 if (more_work_is_plausible)
206 continue; 198 continue;
207 199
208 WaitForWork(); // Wait (sleep) until we have work to do again. 200 WaitForWork(); // Wait (sleep) until we have work to do again.
209 } 201 }
210 } 202 }
211 203
212 void MessagePumpForUI::InitMessageWnd() {
213 // Generate a unique window class name.
214 string16 class_name = StringPrintf(kWndClassFormat, this);
215
216 HINSTANCE instance = CURRENT_MODULE();
217 WNDCLASSEX wc = {0};
218 wc.cbSize = sizeof(wc);
219 wc.lpfnWndProc = base::win::WrappedWindowProc<WndProcThunk>;
220 wc.hInstance = instance;
221 wc.lpszClassName = class_name.c_str();
222 atom_ = RegisterClassEx(&wc);
223 DCHECK(atom_);
224
225 message_hwnd_ = CreateWindowEx(0, MAKEINTATOM(atom_), 0, 0, 0, 0, 0, 0,
226 HWND_MESSAGE, 0, instance, 0);
227 DCHECK(message_hwnd_);
228 }
229
230 void MessagePumpForUI::WaitForWork() { 204 void MessagePumpForUI::WaitForWork() {
231 // Wait until a message is available, up to the time needed by the timer 205 // Wait until a message is available, up to the time needed by the timer
232 // manager to fire the next set of timers. 206 // manager to fire the next set of timers.
233 int delay; 207 int delay;
234 DWORD wait_flags = MWMO_INPUTAVAILABLE; 208 DWORD wait_flags = MWMO_INPUTAVAILABLE;
235 209
236 while ((delay = GetCurrentDelay()) != 0) { 210 while ((delay = GetCurrentDelay()) != 0) {
237 if (delay < 0) // Negative value means no timers waiting. 211 if (delay < 0) // Negative value means no timers waiting.
238 delay = INFINITE; 212 delay = INFINITE;
239 213
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
289 263
290 // Now give the delegate a chance to do some work. It'll let us know if it 264 // Now give the delegate a chance to do some work. It'll let us know if it
291 // needs to do more work. 265 // needs to do more work.
292 if (state_->delegate->DoWork()) 266 if (state_->delegate->DoWork())
293 ScheduleWork(); 267 ScheduleWork();
294 state_->delegate->DoDelayedWork(&delayed_work_time_); 268 state_->delegate->DoDelayedWork(&delayed_work_time_);
295 RescheduleTimer(); 269 RescheduleTimer();
296 } 270 }
297 271
298 void MessagePumpForUI::HandleTimerMessage() { 272 void MessagePumpForUI::HandleTimerMessage() {
299 KillTimer(message_hwnd_, reinterpret_cast<UINT_PTR>(this)); 273 KillTimer(message_window_.hwnd(), reinterpret_cast<UINT_PTR>(this));
300 274
301 // If we are being called outside of the context of Run, then don't do 275 // If we are being called outside of the context of Run, then don't do
302 // anything. This could correspond to a MessageBox call or something of 276 // anything. This could correspond to a MessageBox call or something of
303 // that sort. 277 // that sort.
304 if (!state_) 278 if (!state_)
305 return; 279 return;
306 280
307 state_->delegate->DoDelayedWork(&delayed_work_time_); 281 state_->delegate->DoDelayedWork(&delayed_work_time_);
308 RescheduleTimer(); 282 RescheduleTimer();
309 } 283 }
(...skipping 24 matching lines...) Expand all
334 int delay_msec = GetCurrentDelay(); 308 int delay_msec = GetCurrentDelay();
335 DCHECK_GE(delay_msec, 0); 309 DCHECK_GE(delay_msec, 0);
336 if (delay_msec == 0) { 310 if (delay_msec == 0) {
337 ScheduleWork(); 311 ScheduleWork();
338 } else { 312 } else {
339 if (delay_msec < USER_TIMER_MINIMUM) 313 if (delay_msec < USER_TIMER_MINIMUM)
340 delay_msec = USER_TIMER_MINIMUM; 314 delay_msec = USER_TIMER_MINIMUM;
341 315
342 // Create a WM_TIMER event that will wake us up to check for any pending 316 // Create a WM_TIMER event that will wake us up to check for any pending
343 // timers (in case we are running within a nested, external sub-pump). 317 // timers (in case we are running within a nested, external sub-pump).
344 BOOL ret = SetTimer(message_hwnd_, reinterpret_cast<UINT_PTR>(this), 318 BOOL ret = SetTimer(message_window_.hwnd(), NULL, delay_msec, nullptr);
dcheng 2016/11/08 23:20:39 Should we be consistent about using nullptr?
robliao 2016/11/08 23:31:56 The second argument to SetTimer is a UINT_PTR, whi
345 delay_msec, nullptr);
346 if (ret) 319 if (ret)
347 return; 320 return;
348 // If we can't set timers, we are in big trouble... but cross our fingers 321 // If we can't set timers, we are in big trouble... but cross our fingers
349 // for now. 322 // for now.
350 // TODO(jar): If we don't see this error, use a CHECK() here instead. 323 // TODO(jar): If we don't see this error, use a CHECK() here instead.
351 UMA_HISTOGRAM_ENUMERATION("Chrome.MessageLoopProblem", SET_TIMER_ERROR, 324 UMA_HISTOGRAM_ENUMERATION("Chrome.MessageLoopProblem", SET_TIMER_ERROR,
352 MESSAGE_LOOP_PROBLEM_MAX); 325 MESSAGE_LOOP_PROBLEM_MAX);
353 } 326 }
354 } 327 }
355 328
(...skipping 22 matching lines...) Expand all
378 UMA_HISTOGRAM_ENUMERATION("Chrome.MessageLoopProblem", 351 UMA_HISTOGRAM_ENUMERATION("Chrome.MessageLoopProblem",
379 RECEIVED_WM_QUIT_ERROR, MESSAGE_LOOP_PROBLEM_MAX); 352 RECEIVED_WM_QUIT_ERROR, MESSAGE_LOOP_PROBLEM_MAX);
380 // Repost the QUIT message so that it will be retrieved by the primary 353 // Repost the QUIT message so that it will be retrieved by the primary
381 // GetMessage() loop. 354 // GetMessage() loop.
382 state_->should_quit = true; 355 state_->should_quit = true;
383 PostQuitMessage(static_cast<int>(msg.wParam)); 356 PostQuitMessage(static_cast<int>(msg.wParam));
384 return false; 357 return false;
385 } 358 }
386 359
387 // While running our main message pump, we discard kMsgHaveWork messages. 360 // While running our main message pump, we discard kMsgHaveWork messages.
388 if (msg.message == kMsgHaveWork && msg.hwnd == message_hwnd_) 361 if (msg.message == kMsgHaveWork && msg.hwnd == message_window_.hwnd())
389 return ProcessPumpReplacementMessage(); 362 return ProcessPumpReplacementMessage();
390 363
391 if (CallMsgFilter(const_cast<MSG*>(&msg), kMessageFilterCode)) 364 if (CallMsgFilter(const_cast<MSG*>(&msg), kMessageFilterCode))
392 return true; 365 return true;
393 366
394 TranslateMessage(&msg); 367 TranslateMessage(&msg);
395 DispatchMessage(&msg); 368 DispatchMessage(&msg);
396 369
397 return true; 370 return true;
398 } 371 }
399 372
400 bool MessagePumpForUI::ProcessPumpReplacementMessage() { 373 bool MessagePumpForUI::ProcessPumpReplacementMessage() {
401 // When we encounter a kMsgHaveWork message, this method is called to peek and 374 // When we encounter a kMsgHaveWork message, this method is called to peek and
402 // process a replacement message. The goal is to make the kMsgHaveWork as non- 375 // process a replacement message. The goal is to make the kMsgHaveWork as non-
403 // intrusive as possible, even though a continuous stream of such messages are 376 // intrusive as possible, even though a continuous stream of such messages are
404 // posted. This method carefully peeks a message while there is no chance for 377 // posted. This method carefully peeks a message while there is no chance for
405 // a kMsgHaveWork to be pending, then resets the |have_work_| flag (allowing a 378 // a kMsgHaveWork to be pending, then resets the |have_work_| flag (allowing a
406 // replacement kMsgHaveWork to possibly be posted), and finally dispatches 379 // replacement kMsgHaveWork to possibly be posted), and finally dispatches
407 // that peeked replacement. Note that the re-post of kMsgHaveWork may be 380 // that peeked replacement. Note that the re-post of kMsgHaveWork may be
408 // asynchronous to this thread!! 381 // asynchronous to this thread!!
409 382
410 MSG msg; 383 MSG msg;
411 const bool have_message = 384 const bool have_message =
412 PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE) != FALSE; 385 PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE) != FALSE;
413 386
414 // Expect no message or a message different than kMsgHaveWork. 387 // Expect no message or a message different than kMsgHaveWork.
415 DCHECK(!have_message || kMsgHaveWork != msg.message || 388 DCHECK(!have_message || kMsgHaveWork != msg.message ||
416 msg.hwnd != message_hwnd_); 389 msg.hwnd != message_window_.hwnd());
417 390
418 // Since we discarded a kMsgHaveWork message, we must update the flag. 391 // Since we discarded a kMsgHaveWork message, we must update the flag.
419 int old_work_state_ = InterlockedExchange(&work_state_, READY); 392 int old_work_state_ = InterlockedExchange(&work_state_, READY);
420 DCHECK_EQ(HAVE_WORK, old_work_state_); 393 DCHECK_EQ(HAVE_WORK, old_work_state_);
421 394
422 // We don't need a special time slice if we didn't have_message to process. 395 // We don't need a special time slice if we didn't have_message to process.
423 if (!have_message) 396 if (!have_message)
424 return false; 397 return false;
425 398
426 // Guarantee we'll get another time slice in the case where we go into native 399 // Guarantee we'll get another time slice in the case where we go into native
427 // windows code. This ScheduleWork() may hurt performance a tiny bit when 400 // windows code. This ScheduleWork() may hurt performance a tiny bit when
428 // tasks appear very infrequently, but when the event queue is busy, the 401 // tasks appear very infrequently, but when the event queue is busy, the
429 // kMsgHaveWork events get (percentage wise) rarer and rarer. 402 // kMsgHaveWork events get (percentage wise) rarer and rarer.
430 ScheduleWork(); 403 ScheduleWork();
431 return ProcessMessageHelper(msg); 404 return ProcessMessageHelper(msg);
432 } 405 }
433 406
434 //----------------------------------------------------------------------------- 407 //-----------------------------------------------------------------------------
435 // MessagePumpForGpu public: 408 // MessagePumpForGpu public:
436 409
437 MessagePumpForGpu::MessagePumpForGpu() { 410 MessagePumpForGpu::MessagePumpForGpu() {
438 event_.Set(CreateEvent(nullptr, FALSE, FALSE, nullptr)); 411 event_.Set(CreateEvent(nullptr, FALSE, FALSE, nullptr));
439 } 412 }
440 413
441 MessagePumpForGpu::~MessagePumpForGpu() {} 414 MessagePumpForGpu::~MessagePumpForGpu() = default;
442 415
443 // static 416 // static
444 void MessagePumpForGpu::InitFactory() { 417 void MessagePumpForGpu::InitFactory() {
445 bool init_result = MessageLoop::InitMessagePumpForUIFactory( 418 bool init_result = MessageLoop::InitMessagePumpForUIFactory(
446 &MessagePumpForGpu::CreateMessagePumpForGpu); 419 &MessagePumpForGpu::CreateMessagePumpForGpu);
447 DCHECK(init_result); 420 DCHECK(init_result);
448 } 421 }
449 422
450 // static 423 // static
451 std::unique_ptr<MessagePump> MessagePumpForGpu::CreateMessagePumpForGpu() { 424 std::unique_ptr<MessagePump> MessagePumpForGpu::CreateMessagePumpForGpu() {
(...skipping 290 matching lines...) Expand 10 before | Expand all | Expand 10 after
742 if (!filter || it->handler == filter) { 715 if (!filter || it->handler == filter) {
743 *item = *it; 716 *item = *it;
744 completed_io_.erase(it); 717 completed_io_.erase(it);
745 return true; 718 return true;
746 } 719 }
747 } 720 }
748 return false; 721 return false;
749 } 722 }
750 723
751 } // namespace base 724 } // namespace base
OLDNEW
« no previous file with comments | « base/message_loop/message_pump_win.h ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698