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

Unified Diff: media/audio/win/audio_low_latency_input_win.cc

Issue 2680873002: Add UMA stats for open and fix memory leak (Closed)
Patch Set: Add comment Created 3 years, 10 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
« no previous file with comments | « media/audio/win/audio_low_latency_input_win.h ('k') | tools/metrics/histograms/histograms.xml » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: media/audio/win/audio_low_latency_input_win.cc
diff --git a/media/audio/win/audio_low_latency_input_win.cc b/media/audio/win/audio_low_latency_input_win.cc
index 4e416a8a426ffca5a637227998d53e838d785ade..22355580aac45b2c656ae6066bd25fd1b72f1a3f 100644
--- a/media/audio/win/audio_low_latency_input_win.cc
+++ b/media/audio/win/audio_low_latency_input_win.cc
@@ -7,6 +7,7 @@
#include <memory>
#include "base/logging.h"
+#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "media/audio/audio_device_description.h"
@@ -24,20 +25,10 @@ WASAPIAudioInputStream::WASAPIAudioInputStream(AudioManagerWin* manager,
const AudioParameters& params,
const std::string& device_id)
: manager_(manager),
- capture_thread_(NULL),
- opened_(false),
- started_(false),
- frame_size_(0),
- packet_size_frames_(0),
- packet_size_bytes_(0),
- endpoint_buffer_size_frames_(0),
device_id_(device_id),
- perf_count_to_100ns_units_(0.0),
- ms_to_frame_count_(0.0),
- sink_(NULL),
- audio_bus_(media::AudioBus::Create(params)),
- mute_done_(false) {
+ audio_bus_(media::AudioBus::Create(params)) {
DCHECK(manager_);
+ DCHECK(!device_id_.empty());
// Load the Avrt DLL if not already loaded. Required to support MMCSS.
bool avrt_init = avrt::Initialize();
@@ -89,6 +80,8 @@ WASAPIAudioInputStream::~WASAPIAudioInputStream() {
bool WASAPIAudioInputStream::Open() {
DCHECK(CalledOnValidThread());
+ DCHECK_EQ(OPEN_RESULT_OK, open_result_);
+
// Verify that we are not already opened.
if (opened_)
return false;
@@ -97,32 +90,43 @@ bool WASAPIAudioInputStream::Open() {
// device with the specified unique identifier or role which was
// set at construction.
HRESULT hr = SetCaptureDevice();
- if (FAILED(hr))
+ if (FAILED(hr)) {
+ ReportOpenResult();
return false;
+ }
// Obtain an IAudioClient interface which enables us to create and initialize
// an audio stream between an audio application and the audio engine.
- hr = ActivateCaptureDevice();
- if (FAILED(hr))
+ hr = endpoint_device_->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER,
+ NULL, audio_client_.ReceiveVoid());
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_ACTIVATION_FAILED;
+ ReportOpenResult();
return false;
+ }
+#ifndef NDEBUG
// Retrieve the stream format which the audio engine uses for its internal
// processing/mixing of shared-mode streams. This function call is for
// diagnostic purposes only and only in debug mode.
-#ifndef NDEBUG
hr = GetAudioEngineStreamFormat();
#endif
// Verify that the selected audio endpoint supports the specified format
// set during construction.
- if (!DesiredFormatIsSupported())
+ if (!DesiredFormatIsSupported()) {
+ open_result_ = OPEN_RESULT_FORMAT_NOT_SUPPORTED;
+ ReportOpenResult();
return false;
+ }
// Initialize the audio stream between the client and the device using
// shared mode and a lowest possible glitch-free latency.
hr = InitializeAudioEngine();
-
+ ReportOpenResult(); // Report before we assign a value to |opened_|.
opened_ = SUCCEEDED(hr);
+ DCHECK(open_result_ == OPEN_RESULT_OK || !opened_);
+
return opened_;
}
@@ -159,9 +163,10 @@ void WASAPIAudioInputStream::Start(AudioInputCallback* callback) {
// Create and start the thread that will drive the capturing by waiting for
// capture events.
- capture_thread_ = new base::DelegateSimpleThread(
+ DCHECK(!capture_thread_.get());
+ capture_thread_.reset(new base::DelegateSimpleThread(
this, "wasapi_capture_thread",
- base::SimpleThread::Options(base::ThreadPriority::REALTIME_AUDIO));
+ base::SimpleThread::Options(base::ThreadPriority::REALTIME_AUDIO)));
capture_thread_->Start();
// Start streaming data between the endpoint buffer and the audio engine.
@@ -209,7 +214,7 @@ void WASAPIAudioInputStream::Stop() {
if (capture_thread_) {
SetEvent(stop_capture_event_.Get());
capture_thread_->Join();
- capture_thread_ = NULL;
+ capture_thread_.reset();
}
started_ = false;
@@ -299,8 +304,8 @@ void WASAPIAudioInputStream::Run() {
// Enable MMCSS to ensure that this thread receives prioritized access to
// CPU resources.
DWORD task_index = 0;
- HANDLE mm_task = avrt::AvSetMmThreadCharacteristics(L"Pro Audio",
- &task_index);
+ HANDLE mm_task =
+ avrt::AvSetMmThreadCharacteristics(L"Pro Audio", &task_index);
bool mmcss_is_ok =
(mm_task && avrt::AvSetMmThreadPriority(mm_task, AVRT_PRIORITY_CRITICAL));
if (!mmcss_is_ok) {
@@ -316,17 +321,17 @@ void WASAPIAudioInputStream::Run() {
// 2) The selected buffer size is larger than the recorded buffer size in
// each event.
size_t buffer_frame_index = 0;
- size_t capture_buffer_size = std::max(
- 2 * endpoint_buffer_size_frames_ * frame_size_,
- 2 * packet_size_frames_ * frame_size_);
+ size_t capture_buffer_size =
+ std::max(2 * endpoint_buffer_size_frames_ * frame_size_,
+ 2 * packet_size_frames_ * frame_size_);
std::unique_ptr<uint8_t[]> capture_buffer(new uint8_t[capture_buffer_size]);
LARGE_INTEGER now_count = {};
bool recording = true;
bool error = false;
double volume = GetVolume();
- HANDLE wait_array[2] =
- { stop_capture_event_.Get(), audio_samples_ready_event_.Get() };
+ HANDLE wait_array[2] = {stop_capture_event_.Get(),
+ audio_samples_ready_event_.Get()};
base::win::ScopedComPtr<IAudioClock> audio_clock;
audio_client_->GetService(__uuidof(IAudioClock), audio_clock.ReceiveVoid());
@@ -344,113 +349,109 @@ void WASAPIAudioInputStream::Run() {
// |stop_capture_event_| has been set.
recording = false;
break;
- case WAIT_OBJECT_0 + 1:
- {
- TRACE_EVENT0("audio", "WASAPIAudioInputStream::Run_0");
- // |audio_samples_ready_event_| has been set.
- BYTE* data_ptr = NULL;
- UINT32 num_frames_to_read = 0;
- DWORD flags = 0;
- UINT64 device_position = 0;
- UINT64 first_audio_frame_timestamp = 0;
-
- // Retrieve the amount of data in the capture endpoint buffer,
- // replace it with silence if required, create callbacks for each
- // packet and store non-delivered data for the next event.
- hr = audio_capture_client_->GetBuffer(&data_ptr,
- &num_frames_to_read,
- &flags,
- &device_position,
- &first_audio_frame_timestamp);
- if (FAILED(hr)) {
- DLOG(ERROR) << "Failed to get data from the capture buffer";
- continue;
- }
-
- if (audio_clock) {
- // The reported timestamp from GetBuffer is not as reliable as the
- // clock from the client. We've seen timestamps reported for
- // USB audio devices, be off by several days. Furthermore we've
- // seen them jump back in time every 2 seconds or so.
- audio_clock->GetPosition(
- &device_position, &first_audio_frame_timestamp);
- }
-
-
- if (num_frames_to_read != 0) {
- size_t pos = buffer_frame_index * frame_size_;
- size_t num_bytes = num_frames_to_read * frame_size_;
- DCHECK_GE(capture_buffer_size, pos + num_bytes);
+ case WAIT_OBJECT_0 + 1: {
+ TRACE_EVENT0("audio", "WASAPIAudioInputStream::Run_0");
+ // |audio_samples_ready_event_| has been set.
+ BYTE* data_ptr = NULL;
+ UINT32 num_frames_to_read = 0;
+ DWORD flags = 0;
+ UINT64 device_position = 0;
+ UINT64 first_audio_frame_timestamp = 0;
+
+ // Retrieve the amount of data in the capture endpoint buffer,
+ // replace it with silence if required, create callbacks for each
+ // packet and store non-delivered data for the next event.
+ hr = audio_capture_client_->GetBuffer(&data_ptr, &num_frames_to_read,
+ &flags, &device_position,
+ &first_audio_frame_timestamp);
+ if (FAILED(hr)) {
+ DLOG(ERROR) << "Failed to get data from the capture buffer";
+ continue;
+ }
- if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
- // Clear out the local buffer since silence is reported.
- memset(&capture_buffer[pos], 0, num_bytes);
- } else {
- // Copy captured data from audio engine buffer to local buffer.
- memcpy(&capture_buffer[pos], data_ptr, num_bytes);
- }
+ if (audio_clock) {
+ // The reported timestamp from GetBuffer is not as reliable as the
+ // clock from the client. We've seen timestamps reported for
+ // USB audio devices, be off by several days. Furthermore we've
+ // seen them jump back in time every 2 seconds or so.
+ audio_clock->GetPosition(&device_position,
+ &first_audio_frame_timestamp);
+ }
- buffer_frame_index += num_frames_to_read;
+ if (num_frames_to_read != 0) {
+ size_t pos = buffer_frame_index * frame_size_;
+ size_t num_bytes = num_frames_to_read * frame_size_;
+ DCHECK_GE(capture_buffer_size, pos + num_bytes);
+
+ if (flags & AUDCLNT_BUFFERFLAGS_SILENT) {
+ // Clear out the local buffer since silence is reported.
+ memset(&capture_buffer[pos], 0, num_bytes);
+ } else {
+ // Copy captured data from audio engine buffer to local buffer.
+ memcpy(&capture_buffer[pos], data_ptr, num_bytes);
}
- hr = audio_capture_client_->ReleaseBuffer(num_frames_to_read);
- DLOG_IF(ERROR, FAILED(hr)) << "Failed to release capture buffer";
-
- // Derive a delay estimate for the captured audio packet.
- // The value contains two parts (A+B), where A is the delay of the
- // first audio frame in the packet and B is the extra delay
- // contained in any stored data. Unit is in audio frames.
- QueryPerformanceCounter(&now_count);
- // first_audio_frame_timestamp will be 0 if we didn't get a timestamp.
- double audio_delay_frames = first_audio_frame_timestamp == 0 ?
- num_frames_to_read :
- ((perf_count_to_100ns_units_ * now_count.QuadPart -
- first_audio_frame_timestamp) / 10000.0) * ms_to_frame_count_ +
- buffer_frame_index - num_frames_to_read;
-
- // Get a cached AGC volume level which is updated once every second
- // on the audio manager thread. Note that, |volume| is also updated
- // each time SetVolume() is called through IPC by the render-side AGC.
- GetAgcVolume(&volume);
-
- // Deliver captured data to the registered consumer using a packet
- // size which was specified at construction.
- uint32_t delay_frames =
- static_cast<uint32_t>(audio_delay_frames + 0.5);
- while (buffer_frame_index >= packet_size_frames_) {
- // Copy data to audio bus to match the OnData interface.
- uint8_t* audio_data =
- reinterpret_cast<uint8_t*>(capture_buffer.get());
- audio_bus_->FromInterleaved(
- audio_data, audio_bus_->frames(), format_.wBitsPerSample / 8);
-
- // Deliver data packet, delay estimation and volume level to
- // the user.
- sink_->OnData(
- this, audio_bus_.get(), delay_frames * frame_size_, volume);
-
- // Store parts of the recorded data which can't be delivered
- // using the current packet size. The stored section will be used
- // either in the next while-loop iteration or in the next
- // capture event.
- // TODO(tommi): If this data will be used in the next capture
- // event, we will report incorrect delay estimates because
- // we'll use the one for the captured data that time around
- // (i.e. in the future).
- memmove(&capture_buffer[0],
- &capture_buffer[packet_size_bytes_],
- (buffer_frame_index - packet_size_frames_) * frame_size_);
-
- DCHECK_GE(buffer_frame_index, packet_size_frames_);
- buffer_frame_index -= packet_size_frames_;
- if (delay_frames > packet_size_frames_) {
- delay_frames -= packet_size_frames_;
- } else {
- delay_frames = 0;
- }
+ buffer_frame_index += num_frames_to_read;
+ }
+
+ hr = audio_capture_client_->ReleaseBuffer(num_frames_to_read);
+ DLOG_IF(ERROR, FAILED(hr)) << "Failed to release capture buffer";
+
+ // Derive a delay estimate for the captured audio packet.
+ // The value contains two parts (A+B), where A is the delay of the
+ // first audio frame in the packet and B is the extra delay
+ // contained in any stored data. Unit is in audio frames.
+ QueryPerformanceCounter(&now_count);
+ // first_audio_frame_timestamp will be 0 if we didn't get a timestamp.
+ double audio_delay_frames =
+ first_audio_frame_timestamp == 0
+ ? num_frames_to_read
+ : ((perf_count_to_100ns_units_ * now_count.QuadPart -
+ first_audio_frame_timestamp) /
+ 10000.0) *
+ ms_to_frame_count_ +
+ buffer_frame_index - num_frames_to_read;
+
+ // Get a cached AGC volume level which is updated once every second
+ // on the audio manager thread. Note that, |volume| is also updated
+ // each time SetVolume() is called through IPC by the render-side AGC.
+ GetAgcVolume(&volume);
+
+ // Deliver captured data to the registered consumer using a packet
+ // size which was specified at construction.
+ uint32_t delay_frames = static_cast<uint32_t>(audio_delay_frames + 0.5);
+ while (buffer_frame_index >= packet_size_frames_) {
+ // Copy data to audio bus to match the OnData interface.
+ uint8_t* audio_data =
+ reinterpret_cast<uint8_t*>(capture_buffer.get());
+ audio_bus_->FromInterleaved(audio_data, audio_bus_->frames(),
+ format_.wBitsPerSample / 8);
+
+ // Deliver data packet, delay estimation and volume level to
+ // the user.
+ sink_->OnData(this, audio_bus_.get(), delay_frames * frame_size_,
+ volume);
+
+ // Store parts of the recorded data which can't be delivered
+ // using the current packet size. The stored section will be used
+ // either in the next while-loop iteration or in the next
+ // capture event.
+ // TODO(tommi): If this data will be used in the next capture
+ // event, we will report incorrect delay estimates because
+ // we'll use the one for the captured data that time around
+ // (i.e. in the future).
+ memmove(&capture_buffer[0], &capture_buffer[packet_size_bytes_],
+ (buffer_frame_index - packet_size_frames_) * frame_size_);
+
+ DCHECK_GE(buffer_frame_index, packet_size_frames_);
+ buffer_frame_index -= packet_size_frames_;
+ if (delay_frames > packet_size_frames_) {
+ delay_frames -= packet_size_frames_;
+ } else {
+ delay_frames = 0;
}
}
- break;
+ } break;
default:
error = true;
break;
@@ -477,13 +478,16 @@ void WASAPIAudioInputStream::HandleError(HRESULT err) {
}
HRESULT WASAPIAudioInputStream::SetCaptureDevice() {
+ DCHECK_EQ(OPEN_RESULT_OK, open_result_);
DCHECK(!endpoint_device_.get());
ScopedComPtr<IMMDeviceEnumerator> enumerator;
- HRESULT hr = enumerator.CreateInstance(__uuidof(MMDeviceEnumerator),
- NULL, CLSCTX_INPROC_SERVER);
- if (FAILED(hr))
+ HRESULT hr = enumerator.CreateInstance(__uuidof(MMDeviceEnumerator), NULL,
+ CLSCTX_INPROC_SERVER);
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_CREATE_INSTANCE;
return hr;
+ }
// Retrieve the IMMDevice by using the specified role or the specified
// unique endpoint device-identification string.
@@ -513,34 +517,29 @@ HRESULT WASAPIAudioInputStream::SetCaptureDevice() {
endpoint_device_.Receive());
}
- if (FAILED(hr))
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_NO_ENDPOINT;
return hr;
+ }
// Verify that the audio endpoint device is active, i.e., the audio
// adapter that connects to the endpoint device is present and enabled.
DWORD state = DEVICE_STATE_DISABLED;
hr = endpoint_device_->GetState(&state);
- if (FAILED(hr))
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_NO_STATE;
return hr;
+ }
if (!(state & DEVICE_STATE_ACTIVE)) {
DLOG(ERROR) << "Selected capture device is not active.";
+ open_result_ = OPEN_RESULT_DEVICE_NOT_ACTIVE;
hr = E_ACCESSDENIED;
}
return hr;
}
-HRESULT WASAPIAudioInputStream::ActivateCaptureDevice() {
- // Creates and activates an IAudioClient COM object given the selected
- // capture endpoint device.
- HRESULT hr = endpoint_device_->Activate(__uuidof(IAudioClient),
- CLSCTX_INPROC_SERVER,
- NULL,
- audio_client_.ReceiveVoid());
- return hr;
-}
-
HRESULT WASAPIAudioInputStream::GetAudioEngineStreamFormat() {
HRESULT hr = S_OK;
#ifndef NDEBUG
@@ -551,8 +550,8 @@ HRESULT WASAPIAudioInputStream::GetAudioEngineStreamFormat() {
// An WAVEFORMATEXTENSIBLE structure can specify both the mapping of
// channels to speakers and the number of bits of precision in each sample.
base::win::ScopedCoMem<WAVEFORMATEXTENSIBLE> format_ex;
- hr = audio_client_->GetMixFormat(
- reinterpret_cast<WAVEFORMATEX**>(&format_ex));
+ hr =
+ audio_client_->GetMixFormat(reinterpret_cast<WAVEFORMATEX**>(&format_ex));
// See http://msdn.microsoft.com/en-us/windows/hardware/gg463006#EFH
// for details on the WAVE file format.
@@ -567,10 +566,10 @@ HRESULT WASAPIAudioInputStream::GetAudioEngineStreamFormat() {
DVLOG(2) << " cbSize : " << format.cbSize;
DVLOG(2) << "WAVEFORMATEXTENSIBLE:";
- DVLOG(2) << " wValidBitsPerSample: " <<
- format_ex->Samples.wValidBitsPerSample;
- DVLOG(2) << " dwChannelMask : 0x" << std::hex <<
- format_ex->dwChannelMask;
+ DVLOG(2) << " wValidBitsPerSample: "
+ << format_ex->Samples.wValidBitsPerSample;
+ DVLOG(2) << " dwChannelMask : 0x" << std::hex
+ << format_ex->dwChannelMask;
if (format_ex->SubFormat == KSDATAFORMAT_SUBTYPE_PCM)
DVLOG(2) << " SubFormat : KSDATAFORMAT_SUBTYPE_PCM";
else if (format_ex->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
@@ -593,14 +592,14 @@ bool WASAPIAudioInputStream::DesiredFormatIsSupported() {
// the audio engine can mix only PCM streams.
base::win::ScopedCoMem<WAVEFORMATEX> closest_match;
HRESULT hr = audio_client_->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED,
- &format_,
- &closest_match);
+ &format_, &closest_match);
DLOG_IF(ERROR, hr == S_FALSE) << "Format is not supported "
<< "but a closest match exists.";
return (hr == S_OK);
}
HRESULT WASAPIAudioInputStream::InitializeAudioEngine() {
+ DCHECK_EQ(OPEN_RESULT_OK, open_result_);
DWORD flags;
// Use event-driven mode only fo regular input devices. For loopback the
// EVENTCALLBACK flag is specified when intializing
@@ -625,8 +624,10 @@ HRESULT WASAPIAudioInputStream::InitializeAudioEngine() {
? &kCommunicationsSessionId
: nullptr);
- if (FAILED(hr))
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_AUDIO_CLIENT_INIT_FAILED;
return hr;
+ }
// Retrieve the length of the endpoint buffer shared between the client
// and the audio engine. The buffer length determines the maximum amount
@@ -634,8 +635,10 @@ HRESULT WASAPIAudioInputStream::InitializeAudioEngine() {
// during a single processing pass.
// A typical value is 960 audio frames <=> 20ms @ 48kHz sample rate.
hr = audio_client_->GetBufferSize(&endpoint_buffer_size_frames_);
- if (FAILED(hr))
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_GET_BUFFER_SIZE_FAILED;
return hr;
+ }
DVLOG(1) << "endpoint buffer size: " << endpoint_buffer_size_frames_
<< " [frames]";
@@ -685,15 +688,19 @@ HRESULT WASAPIAudioInputStream::InitializeAudioEngine() {
hr = endpoint_device_->Activate(
__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL,
audio_render_client_for_loopback_.ReceiveVoid());
- if (FAILED(hr))
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_LOOPBACK_ACTIVATE_FAILED;
return hr;
+ }
hr = audio_render_client_for_loopback_->Initialize(
AUDCLNT_SHAREMODE_SHARED,
- AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST,
- 0, 0, &format_, NULL);
- if (FAILED(hr))
+ AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST, 0, 0,
+ &format_, NULL);
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_LOOPBACK_INIT_FAILED;
return hr;
+ }
hr = audio_render_client_for_loopback_->SetEventHandle(
audio_samples_ready_event_.Get());
@@ -701,21 +708,34 @@ HRESULT WASAPIAudioInputStream::InitializeAudioEngine() {
hr = audio_client_->SetEventHandle(audio_samples_ready_event_.Get());
}
- if (FAILED(hr))
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_SET_EVENT_HANDLE;
return hr;
+ }
// Get access to the IAudioCaptureClient interface. This interface
// enables us to read input data from the capture endpoint buffer.
hr = audio_client_->GetService(__uuidof(IAudioCaptureClient),
audio_capture_client_.ReceiveVoid());
- if (FAILED(hr))
+ if (FAILED(hr)) {
+ open_result_ = OPEN_RESULT_NO_CAPTURE_CLIENT;
return hr;
+ }
// Obtain a reference to the ISimpleAudioVolume interface which enables
// us to control the master volume level of an audio session.
hr = audio_client_->GetService(__uuidof(ISimpleAudioVolume),
simple_audio_volume_.ReceiveVoid());
+ if (FAILED(hr))
+ open_result_ = OPEN_RESULT_NO_AUDIO_VOLUME;
+
return hr;
}
+void WASAPIAudioInputStream::ReportOpenResult() const {
+ DCHECK(!opened_); // This method must be called before we set this flag.
+ UMA_HISTOGRAM_ENUMERATION("Media.Audio.Capture.Win.Open", open_result_,
+ OPEN_RESULT_MAX + 1);
+}
+
} // namespace media
« no previous file with comments | « media/audio/win/audio_low_latency_input_win.h ('k') | tools/metrics/histograms/histograms.xml » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698