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

Side by Side Diff: media/capture/content/video_capture_oracle.cc

Issue 2143903003: [WIP] Move media/capture to device/capture (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 4 years, 5 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 (c) 2015 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 "media/capture/content/video_capture_oracle.h"
6
7 #include <algorithm>
8
9 #include "base/format_macros.h"
10 #include "base/numerics/safe_conversions.h"
11 #include "base/strings/stringprintf.h"
12
13 namespace media {
14
15 namespace {
16
17 // When a non-compositor event arrives after animation has halted, this
18 // controls how much time must elapse before deciding to allow a capture.
19 const int kAnimationHaltPeriodBeforeOtherSamplingMicros = 250000;
20
21 // When estimating frame durations, this is the hard upper-bound on the
22 // estimate.
23 const int kUpperBoundDurationEstimateMicros = 1000000; // 1 second
24
25 // The half-life of data points provided to the accumulator used when evaluating
26 // the recent utilization of the buffer pool. This value is based on a
27 // simulation, and reacts quickly to change to avoid depleting the buffer pool
28 // (which would cause hard frame drops).
29 const int kBufferUtilizationEvaluationMicros = 200000; // 0.2 seconds
30
31 // The half-life of data points provided to the accumulator used when evaluating
32 // the recent resource utilization of the consumer. The trade-off made here is
33 // reaction time versus over-reacting to outlier data points.
34 const int kConsumerCapabilityEvaluationMicros = 1000000; // 1 second
35
36 // The minimum amount of time that must pass between changes to the capture
37 // size. This throttles the rate of size changes, to avoid stressing consumers
38 // and to allow the end-to-end system sufficient time to stabilize before
39 // re-evaluating the capture size.
40 const int kMinSizeChangePeriodMicros = 3000000; // 3 seconds
41
42 // The maximum amount of time that may elapse without a feedback update. Any
43 // longer, and currently-accumulated feedback is not considered recent enough to
44 // base decisions off of. This prevents changes to the capture size when there
45 // is an unexpected pause in events.
46 const int kMaxTimeSinceLastFeedbackUpdateMicros = 1000000; // 1 second
47
48 // The amount of time, since the source size last changed, to allow frequent
49 // increases in capture area. This allows the system a period of time to
50 // quickly explore up and down to find an ideal point before being more careful
51 // about capture size increases.
52 const int kExplorationPeriodAfterSourceSizeChangeMicros =
53 3 * kMinSizeChangePeriodMicros;
54
55 // The amount of additional time, since content animation was last detected, to
56 // continue being extra-careful about increasing the capture size. This is used
57 // to prevent breif periods of non-animating content from throwing off the
58 // heuristics that decide whether to increase the capture size.
59 const int kDebouncingPeriodForAnimatedContentMicros = 3000000; // 3 seconds
60
61 // When content is animating, this is the length of time the system must be
62 // contiguously under-utilized before increasing the capture size.
63 const int kProvingPeriodForAnimatedContentMicros = 30000000; // 30 seconds
64
65 // Given the amount of time between frames, compare to the expected amount of
66 // time between frames at |frame_rate| and return the fractional difference.
67 double FractionFromExpectedFrameRate(base::TimeDelta delta, int frame_rate) {
68 DCHECK_GT(frame_rate, 0);
69 const base::TimeDelta expected_delta =
70 base::TimeDelta::FromSeconds(1) / frame_rate;
71 return (delta - expected_delta).InMillisecondsF() /
72 expected_delta.InMillisecondsF();
73 }
74
75 // Returns the next-higher TimeTicks value.
76 // TODO(miu): Patch FeedbackSignalAccumulator reset behavior and remove this
77 // hack.
78 base::TimeTicks JustAfter(base::TimeTicks t) {
79 return t + base::TimeDelta::FromMicroseconds(1);
80 }
81
82 // Returns true if updates have been accumulated by |accumulator| for a
83 // sufficient amount of time and the latest update was fairly recent, relative
84 // to |now|.
85 bool HasSufficientRecentFeedback(
86 const FeedbackSignalAccumulator<base::TimeTicks>& accumulator,
87 base::TimeTicks now) {
88 const base::TimeDelta amount_of_history =
89 accumulator.update_time() - accumulator.reset_time();
90 return (amount_of_history.InMicroseconds() >= kMinSizeChangePeriodMicros) &&
91 ((now - accumulator.update_time()).InMicroseconds() <=
92 kMaxTimeSinceLastFeedbackUpdateMicros);
93 }
94
95 } // anonymous namespace
96
97 VideoCaptureOracle::VideoCaptureOracle(
98 base::TimeDelta min_capture_period,
99 const gfx::Size& max_frame_size,
100 media::ResolutionChangePolicy resolution_change_policy,
101 bool enable_auto_throttling)
102 : auto_throttling_enabled_(enable_auto_throttling),
103 next_frame_number_(0),
104 last_successfully_delivered_frame_number_(-1),
105 num_frames_pending_(0),
106 smoothing_sampler_(min_capture_period),
107 content_sampler_(min_capture_period),
108 resolution_chooser_(max_frame_size, resolution_change_policy),
109 buffer_pool_utilization_(base::TimeDelta::FromMicroseconds(
110 kBufferUtilizationEvaluationMicros)),
111 estimated_capable_area_(base::TimeDelta::FromMicroseconds(
112 kConsumerCapabilityEvaluationMicros)) {
113 VLOG(1) << "Auto-throttling is "
114 << (auto_throttling_enabled_ ? "enabled." : "disabled.");
115 }
116
117 VideoCaptureOracle::~VideoCaptureOracle() {
118 }
119
120 void VideoCaptureOracle::SetSourceSize(const gfx::Size& source_size) {
121 resolution_chooser_.SetSourceSize(source_size);
122 // If the |resolution_chooser_| computed a new capture size, that will become
123 // visible via a future call to ObserveEventAndDecideCapture().
124 source_size_change_time_ = (next_frame_number_ == 0) ?
125 base::TimeTicks() : GetFrameTimestamp(next_frame_number_ - 1);
126 }
127
128 bool VideoCaptureOracle::ObserveEventAndDecideCapture(
129 Event event,
130 const gfx::Rect& damage_rect,
131 base::TimeTicks event_time) {
132 DCHECK_GE(event, 0);
133 DCHECK_LT(event, kNumEvents);
134 if (event_time < last_event_time_[event]) {
135 LOG(WARNING) << "Event time is not monotonically non-decreasing. "
136 << "Deciding not to capture this frame.";
137 return false;
138 }
139 last_event_time_[event] = event_time;
140
141 bool should_sample = false;
142 duration_of_next_frame_ = base::TimeDelta();
143 switch (event) {
144 case kCompositorUpdate: {
145 smoothing_sampler_.ConsiderPresentationEvent(event_time);
146 const bool had_proposal = content_sampler_.HasProposal();
147 content_sampler_.ConsiderPresentationEvent(damage_rect, event_time);
148 if (content_sampler_.HasProposal()) {
149 VLOG_IF(1, !had_proposal) << "Content sampler now detects animation.";
150 should_sample = content_sampler_.ShouldSample();
151 if (should_sample) {
152 event_time = content_sampler_.frame_timestamp();
153 duration_of_next_frame_ = content_sampler_.sampling_period();
154 }
155 last_time_animation_was_detected_ = event_time;
156 } else {
157 VLOG_IF(1, had_proposal) << "Content sampler detects animation ended.";
158 should_sample = smoothing_sampler_.ShouldSample();
159 }
160 break;
161 }
162
163 case kActiveRefreshRequest:
164 case kPassiveRefreshRequest:
165 case kMouseCursorUpdate:
166 // Only allow non-compositor samplings when content has not recently been
167 // animating, and only if there are no samplings currently in progress.
168 if (num_frames_pending_ == 0) {
169 if (!content_sampler_.HasProposal() ||
170 ((event_time - last_time_animation_was_detected_).InMicroseconds() >
171 kAnimationHaltPeriodBeforeOtherSamplingMicros)) {
172 smoothing_sampler_.ConsiderPresentationEvent(event_time);
173 should_sample = smoothing_sampler_.ShouldSample();
174 }
175 }
176 break;
177
178 case kNumEvents:
179 NOTREACHED();
180 break;
181 }
182
183 if (!should_sample)
184 return false;
185
186 // If the exact duration of the next frame has not been determined, estimate
187 // it using the difference between the current and last frame.
188 if (duration_of_next_frame_.is_zero()) {
189 if (next_frame_number_ > 0) {
190 duration_of_next_frame_ =
191 event_time - GetFrameTimestamp(next_frame_number_ - 1);
192 }
193 const base::TimeDelta upper_bound =
194 base::TimeDelta::FromMilliseconds(kUpperBoundDurationEstimateMicros);
195 duration_of_next_frame_ =
196 std::max(std::min(duration_of_next_frame_, upper_bound),
197 smoothing_sampler_.min_capture_period());
198 }
199
200 // Update |capture_size_| and reset all feedback signal accumulators if
201 // either: 1) this is the first frame; or 2) |resolution_chooser_| has an
202 // updated capture size and sufficient time has passed since the last size
203 // change.
204 if (next_frame_number_ == 0) {
205 CommitCaptureSizeAndReset(event_time - duration_of_next_frame_);
206 } else if (capture_size_ != resolution_chooser_.capture_size()) {
207 const base::TimeDelta time_since_last_change =
208 event_time - buffer_pool_utilization_.reset_time();
209 if (time_since_last_change.InMicroseconds() >= kMinSizeChangePeriodMicros)
210 CommitCaptureSizeAndReset(GetFrameTimestamp(next_frame_number_ - 1));
211 }
212
213 SetFrameTimestamp(next_frame_number_, event_time);
214 return true;
215 }
216
217 int VideoCaptureOracle::RecordCapture(double pool_utilization) {
218 DCHECK(std::isfinite(pool_utilization) && pool_utilization >= 0.0);
219
220 smoothing_sampler_.RecordSample();
221 const base::TimeTicks timestamp = GetFrameTimestamp(next_frame_number_);
222 content_sampler_.RecordSample(timestamp);
223
224 if (auto_throttling_enabled_) {
225 buffer_pool_utilization_.Update(pool_utilization, timestamp);
226 AnalyzeAndAdjust(timestamp);
227 }
228
229 num_frames_pending_++;
230 return next_frame_number_++;
231 }
232
233 void VideoCaptureOracle::RecordWillNotCapture(double pool_utilization) {
234 VLOG(1) << "Client rejects proposal to capture frame (at #"
235 << next_frame_number_ << ").";
236
237 if (auto_throttling_enabled_) {
238 DCHECK(std::isfinite(pool_utilization) && pool_utilization >= 0.0);
239 const base::TimeTicks timestamp = GetFrameTimestamp(next_frame_number_);
240 buffer_pool_utilization_.Update(pool_utilization, timestamp);
241 AnalyzeAndAdjust(timestamp);
242 }
243
244 // Note: Do not advance |next_frame_number_| since it will be re-used for the
245 // next capture proposal.
246 }
247
248 bool VideoCaptureOracle::CompleteCapture(int frame_number,
249 bool capture_was_successful,
250 base::TimeTicks* frame_timestamp) {
251 num_frames_pending_--;
252 DCHECK_GE(num_frames_pending_, 0);
253
254 // Drop frame if previously delivered frame number is higher.
255 if (last_successfully_delivered_frame_number_ > frame_number) {
256 LOG_IF(WARNING, capture_was_successful)
257 << "Out of order frame delivery detected (have #" << frame_number
258 << ", last was #" << last_successfully_delivered_frame_number_
259 << "). Dropping frame.";
260 return false;
261 }
262
263 if (!IsFrameInRecentHistory(frame_number)) {
264 LOG(WARNING) << "Very old capture being ignored: frame #" << frame_number;
265 return false;
266 }
267
268 if (!capture_was_successful) {
269 VLOG(2) << "Capture of frame #" << frame_number << " was not successful.";
270 return false;
271 }
272
273 DCHECK_NE(last_successfully_delivered_frame_number_, frame_number);
274 last_successfully_delivered_frame_number_ = frame_number;
275
276 *frame_timestamp = GetFrameTimestamp(frame_number);
277
278 // If enabled, log a measurement of how this frame timestamp has incremented
279 // in relation to an ideal increment.
280 if (VLOG_IS_ON(3) && frame_number > 0) {
281 const base::TimeDelta delta =
282 *frame_timestamp - GetFrameTimestamp(frame_number - 1);
283 if (content_sampler_.HasProposal()) {
284 const double estimated_frame_rate =
285 1000000.0 / content_sampler_.detected_period().InMicroseconds();
286 const int rounded_frame_rate =
287 static_cast<int>(estimated_frame_rate + 0.5);
288 VLOG_STREAM(3) << base::StringPrintf(
289 "Captured #%d: delta=%" PRId64
290 " usec"
291 ", now locked into {%s}, %+0.1f%% slower than %d FPS",
292 frame_number, delta.InMicroseconds(),
293 content_sampler_.detected_region().ToString().c_str(),
294 100.0 * FractionFromExpectedFrameRate(delta, rounded_frame_rate),
295 rounded_frame_rate);
296 } else {
297 VLOG_STREAM(3) << base::StringPrintf(
298 "Captured #%d: delta=%" PRId64
299 " usec"
300 ", d/30fps=%+0.1f%%, d/25fps=%+0.1f%%, d/24fps=%+0.1f%%",
301 frame_number, delta.InMicroseconds(),
302 100.0 * FractionFromExpectedFrameRate(delta, 30),
303 100.0 * FractionFromExpectedFrameRate(delta, 25),
304 100.0 * FractionFromExpectedFrameRate(delta, 24));
305 }
306 }
307
308 return true;
309 }
310
311 void VideoCaptureOracle::RecordConsumerFeedback(int frame_number,
312 double resource_utilization) {
313 if (!auto_throttling_enabled_)
314 return;
315
316 if (!std::isfinite(resource_utilization)) {
317 LOG(DFATAL) << "Non-finite utilization provided by consumer for frame #"
318 << frame_number << ": " << resource_utilization;
319 return;
320 }
321 if (resource_utilization <= 0.0)
322 return; // Non-positive values are normal, meaning N/A.
323
324 if (!IsFrameInRecentHistory(frame_number)) {
325 VLOG(1) << "Very old frame feedback being ignored: frame #" << frame_number;
326 return;
327 }
328 const base::TimeTicks timestamp = GetFrameTimestamp(frame_number);
329
330 // Translate the utilization metric to be in terms of the capable frame area
331 // and update the feedback accumulators. Research suggests utilization is at
332 // most linearly proportional to area, and typically is sublinear. Either
333 // way, the end-to-end system should converge to the right place using the
334 // more-conservative assumption (linear).
335 const int area_at_full_utilization =
336 base::saturated_cast<int>(capture_size_.GetArea() / resource_utilization);
337 estimated_capable_area_.Update(area_at_full_utilization, timestamp);
338 }
339
340 // static
341 const char* VideoCaptureOracle::EventAsString(Event event) {
342 switch (event) {
343 case kCompositorUpdate:
344 return "compositor";
345 case kActiveRefreshRequest:
346 return "active_refresh";
347 case kPassiveRefreshRequest:
348 return "passive_refresh";
349 case kMouseCursorUpdate:
350 return "mouse";
351 case kNumEvents:
352 break;
353 }
354 NOTREACHED();
355 return "unknown";
356 }
357
358 base::TimeTicks VideoCaptureOracle::GetFrameTimestamp(int frame_number) const {
359 DCHECK(IsFrameInRecentHistory(frame_number));
360 return frame_timestamps_[frame_number % kMaxFrameTimestamps];
361 }
362
363 void VideoCaptureOracle::SetFrameTimestamp(int frame_number,
364 base::TimeTicks timestamp) {
365 DCHECK(IsFrameInRecentHistory(frame_number));
366 frame_timestamps_[frame_number % kMaxFrameTimestamps] = timestamp;
367 }
368
369 bool VideoCaptureOracle::IsFrameInRecentHistory(int frame_number) const {
370 // Adding (next_frame_number_ >= 0) helps the compiler deduce that there
371 // is no possibility of overflow here.
372 return (frame_number >= 0 && next_frame_number_ >= 0 &&
373 frame_number <= next_frame_number_ &&
374 (next_frame_number_ - frame_number) < kMaxFrameTimestamps);
375 }
376
377 void VideoCaptureOracle::CommitCaptureSizeAndReset(
378 base::TimeTicks last_frame_time) {
379 capture_size_ = resolution_chooser_.capture_size();
380 VLOG(2) << "Now proposing a capture size of " << capture_size_.ToString();
381
382 // Reset each short-term feedback accumulator with a stable-state starting
383 // value.
384 const base::TimeTicks ignore_before_time = JustAfter(last_frame_time);
385 buffer_pool_utilization_.Reset(1.0, ignore_before_time);
386 estimated_capable_area_.Reset(capture_size_.GetArea(), ignore_before_time);
387 }
388
389 void VideoCaptureOracle::AnalyzeAndAdjust(const base::TimeTicks analyze_time) {
390 DCHECK(auto_throttling_enabled_);
391
392 const int decreased_area = AnalyzeForDecreasedArea(analyze_time);
393 if (decreased_area > 0) {
394 resolution_chooser_.SetTargetFrameArea(decreased_area);
395 return;
396 }
397
398 const int increased_area = AnalyzeForIncreasedArea(analyze_time);
399 if (increased_area > 0) {
400 resolution_chooser_.SetTargetFrameArea(increased_area);
401 return;
402 }
403
404 // Explicitly set the target frame area to the current capture area. This
405 // cancels-out the results of a previous call to this method, where the
406 // |resolution_chooser_| may have been instructed to increase or decrease the
407 // capture size. Conditions may have changed since then which indicate no
408 // change should be committed (via CommitCaptureSizeAndReset()).
409 resolution_chooser_.SetTargetFrameArea(capture_size_.GetArea());
410 }
411
412 int VideoCaptureOracle::AnalyzeForDecreasedArea(base::TimeTicks analyze_time) {
413 const int current_area = capture_size_.GetArea();
414 DCHECK_GT(current_area, 0);
415
416 // Translate the recent-average buffer pool utilization to be in terms of
417 // "capable number of pixels per frame," for an apples-to-apples comparison
418 // below.
419 int buffer_capable_area;
420 if (HasSufficientRecentFeedback(buffer_pool_utilization_, analyze_time) &&
421 buffer_pool_utilization_.current() > 1.0) {
422 // This calculation is hand-wavy, but seems to work well in a variety of
423 // situations.
424 buffer_capable_area =
425 static_cast<int>(current_area / buffer_pool_utilization_.current());
426 } else {
427 buffer_capable_area = current_area;
428 }
429
430 int consumer_capable_area;
431 if (HasSufficientRecentFeedback(estimated_capable_area_, analyze_time)) {
432 consumer_capable_area =
433 base::saturated_cast<int>(estimated_capable_area_.current());
434 } else {
435 consumer_capable_area = current_area;
436 }
437
438 // If either of the "capable areas" is less than the current capture area,
439 // decrease the capture area by AT LEAST one step.
440 int decreased_area = -1;
441 const int capable_area = std::min(buffer_capable_area, consumer_capable_area);
442 if (capable_area < current_area) {
443 decreased_area = std::min(
444 capable_area,
445 resolution_chooser_.FindSmallerFrameSize(current_area, 1).GetArea());
446 VLOG_IF(2, !start_time_of_underutilization_.is_null())
447 << "Contiguous period of under-utilization ends: "
448 "System is suddenly over-utilized.";
449 start_time_of_underutilization_ = base::TimeTicks();
450 VLOG(2) << "Proposing a "
451 << (100.0 * (current_area - decreased_area) / current_area)
452 << "% decrease in capture area. :-(";
453 }
454
455 // Always log the capability interpretations at verbose logging level 3. At
456 // level 2, only log when when proposing a decreased area.
457 VLOG(decreased_area == -1 ? 3 : 2)
458 << "Capability of pool=" << (100.0 * buffer_capable_area / current_area)
459 << "%, consumer=" << (100.0 * consumer_capable_area / current_area)
460 << '%';
461
462 return decreased_area;
463 }
464
465 int VideoCaptureOracle::AnalyzeForIncreasedArea(base::TimeTicks analyze_time) {
466 // Compute what one step up in capture size/area would be. If the current
467 // area is already at the maximum, no further analysis is necessary.
468 const int current_area = capture_size_.GetArea();
469 const int increased_area =
470 resolution_chooser_.FindLargerFrameSize(current_area, 1).GetArea();
471 if (increased_area <= current_area)
472 return -1;
473
474 // Determine whether the buffer pool could handle an increase in area.
475 if (!HasSufficientRecentFeedback(buffer_pool_utilization_, analyze_time))
476 return -1;
477 if (buffer_pool_utilization_.current() > 0.0) {
478 const int buffer_capable_area = base::saturated_cast<int>(
479 current_area / buffer_pool_utilization_.current());
480 if (buffer_capable_area < increased_area) {
481 VLOG_IF(2, !start_time_of_underutilization_.is_null())
482 << "Contiguous period of under-utilization ends: "
483 "Buffer pool is no longer under-utilized.";
484 start_time_of_underutilization_ = base::TimeTicks();
485 return -1; // Buffer pool is not under-utilized.
486 }
487 }
488
489 // Determine whether the consumer could handle an increase in area.
490 if (HasSufficientRecentFeedback(estimated_capable_area_, analyze_time)) {
491 if (estimated_capable_area_.current() < increased_area) {
492 VLOG_IF(2, !start_time_of_underutilization_.is_null())
493 << "Contiguous period of under-utilization ends: "
494 "Consumer is no longer under-utilized.";
495 start_time_of_underutilization_ = base::TimeTicks();
496 return -1; // Consumer is not under-utilized.
497 }
498 } else if (estimated_capable_area_.update_time() ==
499 estimated_capable_area_.reset_time()) {
500 // The consumer does not provide any feedback. In this case, the consumer's
501 // capability isn't a consideration.
502 } else {
503 // Consumer is providing feedback, but hasn't reported it recently. Just in
504 // case it's stalled, don't make things worse by increasing the capture
505 // area.
506 return -1;
507 }
508
509 // At this point, the system is currently under-utilized. Reset the start
510 // time if the system was not under-utilized when the last analysis was made.
511 if (start_time_of_underutilization_.is_null())
512 start_time_of_underutilization_ = analyze_time;
513
514 // If the under-utilization started soon after the last source size change,
515 // permit an immediate increase in the capture area. This allows the system
516 // to quickly step-up to an ideal point.
517 if ((start_time_of_underutilization_ -
518 source_size_change_time_).InMicroseconds() <=
519 kExplorationPeriodAfterSourceSizeChangeMicros) {
520 VLOG(2) << "Proposing a "
521 << (100.0 * (increased_area - current_area) / current_area)
522 << "% increase in capture area after source size change. :-)";
523 return increased_area;
524 }
525
526 // While content is animating, require a "proving period" of contiguous
527 // under-utilization before increasing the capture area. This will mitigate
528 // the risk of frames getting dropped when the data volume increases.
529 if ((analyze_time - last_time_animation_was_detected_).InMicroseconds() <
530 kDebouncingPeriodForAnimatedContentMicros) {
531 if ((analyze_time - start_time_of_underutilization_).InMicroseconds() <
532 kProvingPeriodForAnimatedContentMicros) {
533 // Content is animating but the system needs to be under-utilized for a
534 // longer period of time.
535 return -1;
536 } else {
537 // Content is animating and the system has been contiguously
538 // under-utilized for a good long time.
539 VLOG(2) << "Proposing a *cautious* "
540 << (100.0 * (increased_area - current_area) / current_area)
541 << "% increase in capture area while content is animating. :-)";
542 // Reset the "proving period."
543 start_time_of_underutilization_ = base::TimeTicks();
544 return increased_area;
545 }
546 }
547
548 // Content is not animating, so permit an immediate increase in the capture
549 // area. This allows the system to quickly improve the quality of
550 // non-animating content (frame drops are not much of a concern).
551 VLOG(2) << "Proposing a "
552 << (100.0 * (increased_area - current_area) / current_area)
553 << "% increase in capture area for non-animating content. :-)";
554 return increased_area;
555 }
556
557 } // namespace media
OLDNEW
« no previous file with comments | « media/capture/content/video_capture_oracle.h ('k') | media/capture/content/video_capture_oracle_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698