Commit 67932a99 authored by Zhenyao Mo's avatar Zhenyao Mo Committed by Commit Bot

Test videos trigger the expected presentation mode on Windows.

We add a trace event when we present the video swapchain. These events
includes presentation mode and pixel format. Based on existence of such
events and their params, we can decide if the expected video frame
presentation happened.

Note that we found out that GetFrameStatisticsMedia is flaky and this is
out of our control. So if a test fails because of this, we repeat it a
couple times.

BUG=867136
TEST=GPU bots
R=kbr@chromium.org,piman@chromium.org,sunnyps@chromium.org

Change-Id: I1f0972f2887a72ad46f533edda274ffc7608790b
Reviewed-on: https://chromium-review.googlesource.com/c/1441374Reviewed-by: default avatarSunny Sachanandani <sunnyps@chromium.org>
Reviewed-by: default avatarAntoine Labour <piman@chromium.org>
Reviewed-by: default avatarKenneth Russell <kbr@chromium.org>
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#628563}
parent 921ff0b9
......@@ -13,6 +13,13 @@ from gpu_tests import gpu_test_expectations
_START_BROWSER_RETRIES = 3
# Please expand the following lists when we expand to new bot configs.
_SUPPORTED_WIN_VERSIONS = ['win7', 'win10']
_SUPPORTED_WIN_VERSIONS_WITH_DIRECT_COMPOSITION = ['win10']
_SUPPORTED_WIN_GPU_VENDORS = [0x8086, 0x10de, 0x1002]
_SUPPORTED_WIN_INTEL_GPUS = [0x5912]
_SUPPORTED_WIN_INTEL_GPUS_WITH_YUY2_OVERLAYS = [0x5912]
_SUPPORTED_WIN_INTEL_GPUS_WITH_NV12_OVERLAYS = [0x5912]
class GpuIntegrationTest(
serially_executed_browser_test_case.SeriallyExecutedBrowserTestCase):
......@@ -254,6 +261,50 @@ class GpuIntegrationTest(
"""
raise NotImplementedError
def GetOverlayBotConfig(self):
"""Returns expected bot config for DirectComposition and overlay support.
This is only meaningful on Windows platform.
The rules to determine bot config are:
1) Only win10 or newer supports DirectComposition
2) Only Intel supports hardware overlays with DirectComposition
3) Currently the Win/Intel GPU bot supports YUY2 and NV12 overlays
"""
if self.browser is None:
raise Exception("Browser doesn't exist")
system_info = self.browser.GetSystemInfo()
if system_info is None:
raise Exception("Browser doesn't support GetSystemInfo")
gpu = system_info.gpu.devices[0]
if gpu is None:
raise Exception("System Info doesn't have a gpu")
gpu_vendor_id = gpu.vendor_id
gpu_device_id = gpu.device_id
os_version = self.browser.platform.GetOSVersionName()
if os_version is None:
raise Exception("browser.platform.GetOSVersionName() returns None")
os_version = os_version.lower()
config = {
'direct_composition': False,
'supports_overlays': False,
'overlay_cap_yuy2': 'NONE',
'overlay_cap_nv12': 'NONE',
}
assert os_version in _SUPPORTED_WIN_VERSIONS
assert gpu_vendor_id in _SUPPORTED_WIN_GPU_VENDORS
if os_version in _SUPPORTED_WIN_VERSIONS_WITH_DIRECT_COMPOSITION:
config['direct_composition'] = True
if gpu_vendor_id == 0x8086:
config['supports_overlays'] = True
assert gpu_device_id in _SUPPORTED_WIN_INTEL_GPUS
if gpu_device_id in _SUPPORTED_WIN_INTEL_GPUS_WITH_YUY2_OVERLAYS:
config['overlay_cap_yuy2'] = 'SCALING'
if gpu_device_id in _SUPPORTED_WIN_INTEL_GPUS_WITH_NV12_OVERLAYS:
config['overlay_cap_nv12'] = 'SCALING'
return config
@classmethod
def GetExpectations(cls):
if not cls._cached_expectations:
......
......@@ -7,14 +7,6 @@ from gpu_tests.gpu_test_expectations import GpuTestExpectations
import sys
# Please expand the following lists when we expand to new bot configs.
_SUPPORTED_WIN_VERSIONS = ['win7', 'win10']
_SUPPORTED_WIN_VERSIONS_WITH_DIRECT_COMPOSITION = ['win10']
_SUPPORTED_WIN_GPU_VENDORS = [0x8086, 0x10de, 0x1002]
_SUPPORTED_WIN_INTEL_GPUS = [0x5912]
_SUPPORTED_WIN_INTEL_GPUS_WITH_YUY2_OVERLAYS = [0x5912]
_SUPPORTED_WIN_INTEL_GPUS_WITH_NV12_OVERLAYS = [0x5912]
# There are no expectations for info_collection
class InfoCollectionExpectations(GpuTestExpectations):
def SetExpectations(self):
......@@ -43,32 +35,6 @@ class InfoCollectionTest(gpu_integration_test.GpuIntegrationTest):
cls.CustomizeBrowserArgs([])
cls.StartBrowser()
def _GetOverlayExpectations(self, os_version, gpu_vendor_id, gpu_device_id):
# The rules to set up per bot expectations are:
# 1) Only win10 or newer supports DirectComposition
# 2) Only Intel supports hardware overlays with DirectComposition
# 3) Currently the Win/Intel GPU bot supports YUY2 and NV12 overlays
expectations = {
'direct_composition': False,
'supports_overlays': False,
'overlay_cap_yuy2': 'NONE',
'overlay_cap_nv12': 'NONE',
}
assert os_version is not None
os_version = os_version.lower()
assert os_version in _SUPPORTED_WIN_VERSIONS
assert gpu_vendor_id in _SUPPORTED_WIN_GPU_VENDORS
if os_version in _SUPPORTED_WIN_VERSIONS_WITH_DIRECT_COMPOSITION:
expectations['direct_composition'] = True
if gpu_vendor_id == 0x8086:
expectations['supports_overlays'] = True
assert gpu_device_id in _SUPPORTED_WIN_INTEL_GPUS
if gpu_device_id in _SUPPORTED_WIN_INTEL_GPUS_WITH_YUY2_OVERLAYS:
expectations['overlay_cap_yuy2'] = 'SCALING'
if gpu_device_id in _SUPPORTED_WIN_INTEL_GPUS_WITH_NV12_OVERLAYS:
expectations['overlay_cap_nv12'] = 'SCALING'
return expectations
def RunActualGpuTest(self, test_path, *args):
# Make sure the GPU process is started
self.tab.action_runner.Navigate('chrome:gpu')
......@@ -103,15 +69,13 @@ class InfoCollectionTest(gpu_integration_test.GpuIntegrationTest):
os_name = self.browser.platform.GetOSName()
if os_name and os_name.lower() == 'win':
expectations = self._GetOverlayExpectations(
self.browser.platform.GetOSVersionName(),
detected_vendor_id, detected_device_id)
overlay_bot_config = self.GetOverlayBotConfig()
aux_attributes = system_info.gpu.aux_attributes
if not aux_attributes:
self.fail('GPU info does not have aux_attributes.')
for (field, expected) in expectations.iteritems():
for field, expected in overlay_bot_config.iteritems():
detected = aux_attributes.get(field, 'NONE')
if expected != detected:
self.fail('%s mismatch, expected %s but got %s.' %
......
......@@ -10,7 +10,8 @@ class PixelTestPage(object):
"""
def __init__(self, url, name, test_rect, revision,
tolerance=2, browser_args=None, expected_colors=None,
gpu_process_disabled=False, optional_action=None):
gpu_process_disabled=False, optional_action=None,
other_args=None):
super(PixelTestPage, self).__init__()
self.url = url
self.name = name
......@@ -35,6 +36,8 @@ class PixelTestPage(object):
# action here is "CrashGpuProcess" then it would be defined in a
# "_CrashGpuProcess" method in PixelIntegrationTest.
self.optional_action = optional_action
# Whatever other settings a test need to specify.
self.other_args = other_args
def CopyWithNewBrowserArgsAndSuffix(self, browser_args, suffix):
return PixelTestPage(
......@@ -1269,8 +1272,8 @@ def DirectCompositionPages(base_name):
'size': [55, 110],
'color': [12, 12, 255],
'tolerance': tolerance_dc
}
]),
}],
other_args={'video_is_rotated': True}),
PixelTestPage(
'pixel_video_mp4_four_colors_rot_180.html',
......@@ -1306,8 +1309,8 @@ def DirectCompositionPages(base_name):
'size': [110, 57],
'color': [255, 255, 15],
'tolerance': tolerance_dc
}
]),
}],
other_args={'video_is_rotated': True}),
PixelTestPage(
'pixel_video_mp4_four_colors_rot_270.html',
......@@ -1357,8 +1360,8 @@ def DirectCompositionPages(base_name):
'size': [55, 110],
'color': [255, 17, 24],
'tolerance': tolerance_dc
}
]),
}],
other_args={'video_is_rotated': True}),
PixelTestPage(
'pixel_video_vp9.html',
......@@ -1526,6 +1529,6 @@ def DirectCompositionPages(base_name):
'size': [65, 30],
'color': [44, 255, 16],
'tolerance': tolerance_dc
}
]),
}],
other_args={'video_is_rotated': True}),
]
......@@ -2,6 +2,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import os
import sys
......@@ -13,9 +14,6 @@ from gpu_tests import trace_test_expectations
from telemetry.timeline import model as model_module
from telemetry.timeline import tracing_config
TOPLEVEL_SERVICE_CATEGORY = 'disabled-by-default-gpu.service'
TOPLEVEL_DEVICE_CATEGORY = 'disabled-by-default-gpu.device'
gpu_relative_path = "content/test/data/gpu/"
data_paths = [os.path.join(
......@@ -50,6 +48,49 @@ webgl_test_harness_script = r"""
window.domAutomationController = domAutomationController;
"""
basic_test_harness_script = r"""
var domAutomationController = {};
domAutomationController._proceed = false;
domAutomationController._readyForActions = false;
domAutomationController._succeeded = false;
domAutomationController._finished = false;
domAutomationController.send = function(msg) {
domAutomationController._proceed = true;
let lmsg = msg.toLowerCase();
if (lmsg == "ready") {
domAutomationController._readyForActions = true;
} else {
domAutomationController._finished = true;
if (lmsg == "success") {
domAutomationController._succeeded = true;
} else {
domAutomationController._succeeded = false;
}
}
}
window.domAutomationController = domAutomationController;
"""
# Presentation mode enums match DXGI_FRAME_PRESENTATION_MODE
_SWAP_CHAIN_PRESENTATION_MODE_COMPOSED = 0
_SWAP_CHAIN_PRESENTATION_MODE_OVERLAY = 1
_SWAP_CHAIN_PRESENTATION_MODE_NONE = 2
_SWAP_CHAIN_PRESENTATION_MODE_COMPOSITION_FAILURE = 3
# Pixel format enums match OverlayFormat in config/gpu/gpu_info.h
_SWAP_CHAIN_PIXEL_FORMAT_BGRA = 0
_SWAP_CHAIN_PIXEL_FORMAT_YUY2 = 1
_SWAP_CHAIN_PIXEL_FORMAT_NV12 = 2
_TEST_MAX_REPEATS = 30
_TEST_DONE = 0 # Test finished, either failed or succeeded
_TEST_REPEAT = 1 # Test failed, but it's flaky and should run again.
class TraceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
"""Tests GPU traces are plumbed through properly.
......@@ -67,17 +108,31 @@ class TraceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
for p in pixel_test_pages.DefaultPages('TraceTest'):
yield (p.name, gpu_relative_path + p.url,
{'browser_args': [],
'category': TOPLEVEL_SERVICE_CATEGORY,
'category': cls._DisabledByDefaultTraceCategory('gpu.service'),
'test_harness_script': webgl_test_harness_script,
'finish_js_condition': 'domAutomationController._finished',
'expected_event_args': {'gl_category': 'gpu_toplevel'}})
'success_eval_func': 'CheckGLCategory'})
for p in pixel_test_pages.DefaultPages('DeviceTraceTest'):
yield (p.name, gpu_relative_path + p.url,
{'browser_args': [],
'category': TOPLEVEL_DEVICE_CATEGORY,
'category': cls._DisabledByDefaultTraceCategory('gpu.device'),
'test_harness_script': webgl_test_harness_script,
'finish_js_condition': 'domAutomationController._finished',
'expected_event_args': {'gl_category': 'gpu_toplevel'}})
'success_eval_func': 'CheckGLCategory'})
for p in pixel_test_pages.DirectCompositionPages('VideoTraceTest'):
success_eval_func = 'CheckVideoMode'
if (p.other_args is not None and
p.other_args.get('video_is_rotated', False)):
# On several Intel GPUs we tested that support hardware overlays,
# none of them promote a swap chain to hardware overlay if there is
# rotation.
success_eval_func = 'CheckVideoModeNoOverlay'
yield (p.name, gpu_relative_path + p.url,
{'browser_args': p.browser_args,
'category': cls._DisabledByDefaultTraceCategory('gpu.service'),
'test_harness_script': basic_test_harness_script,
'finish_js_condition': 'domAutomationController._finished',
'success_eval_func': success_eval_func})
def RunActualGpuTest(self, test_path, *args):
test_params = args[0]
......@@ -85,13 +140,16 @@ class TraceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
assert 'category' in test_params
assert 'test_harness_script' in test_params
assert 'finish_js_condition' in test_params
assert 'expected_event_args' in test_params
browser_args = test_params['browser_args']
category = test_params['category']
test_harness_script = test_params['test_harness_script']
finish_js_condition = test_params['finish_js_condition']
expected_event_args = test_params['expected_event_args']
success_eval_func = test_params['success_eval_func']
# Maximum repeat a flaky test 30 times
for ii in range(_TEST_MAX_REPEATS):
if ii > 0:
logging.info('Try the test again: #%d', ii + 1)
# The version of this test in the old GPU test harness restarted
# the browser after each test, so continue to do that to match its
# behavior.
......@@ -118,22 +176,15 @@ class TraceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
timeline_model = model_module.TimelineModel(timeline_data)
event_iter = timeline_model.IterAllEvents(
event_type_predicate=timeline_model.IsSliceOrAsyncSlice)
self._EvaluateSuccess(event_iter, category, expected_event_args)
def _EvaluateSuccess(self, event_iterator, expected_category_name,
expected_event_args):
for event in event_iterator:
if event.category != expected_category_name:
continue
for arg_name, arg_value in expected_event_args.iteritems():
if event.args.get(arg_name, None) != arg_value:
test_result = _TEST_DONE
if success_eval_func:
prefixed_func_name = '_EvaluateSuccess_' + success_eval_func
test_result = getattr(self, prefixed_func_name)(category, event_iter)
assert test_result in [_TEST_DONE, _TEST_REPEAT]
if test_result == _TEST_DONE:
break
else:
print 'Found event with category name ' + expected_category_name
break
else:
self.fail('Trace markers for GPU category were not found: %s' %
expected_category_name)
self.fail('Test failed all %d tries' % _TEST_MAX_REPEATS)
@classmethod
def _CreateExpectations(cls):
......@@ -154,6 +205,162 @@ class TraceIntegrationTest(gpu_integration_test.GpuIntegrationTest):
'--enable-logging',
'--enable-experimental-web-platform-features'] + browser_args
def _GetOverlayBotConfigHelper(self):
system_info = self.browser.GetSystemInfo()
if not system_info:
raise Exception("Browser doesn't support GetSystemInfo")
gpu = system_info.gpu.devices[0]
if not gpu:
raise Exception("System Info doesn't have a gpu")
os_version_name = self.browser.platform.GetOSVersionName()
return self.GetOverlayBotConfig(
os_version_name, gpu.vendor_id, gpu.device_id)
@staticmethod
def _SwapChainPixelFormatToStr(pixel_format):
if pixel_format == _SWAP_CHAIN_PIXEL_FORMAT_BGRA:
return 'BGRA'
if pixel_format == _SWAP_CHAIN_PIXEL_FORMAT_YUY2:
return 'YUY2'
if pixel_format == _SWAP_CHAIN_PIXEL_FORMAT_NV12:
return 'NV12'
return str(pixel_format)
@staticmethod
def _SwapChainPixelFormatListToStr(pixel_format_list):
assert len(pixel_format_list) > 0
list_str = None
for pixel_format in pixel_format_list:
format_str = TraceIntegrationTest._SwapChainPixelFormatToStr(pixel_format)
if list_str is not None:
list_str = '%s, %s' % (list_str, format_str)
else:
list_str = format_str
return '[%s]' % list_str
@staticmethod
def _SwapChainPresentationModeToStr(presentation_mode):
if presentation_mode == _SWAP_CHAIN_PRESENTATION_MODE_COMPOSED:
return 'COMPOSED'
if presentation_mode == _SWAP_CHAIN_PRESENTATION_MODE_OVERLAY:
return 'OVERLAY'
if presentation_mode == _SWAP_CHAIN_PRESENTATION_MODE_NONE:
return 'NONE'
if presentation_mode == _SWAP_CHAIN_PRESENTATION_MODE_COMPOSITION_FAILURE:
return 'COMPOSITION_FAILURE'
return str(presentation_mode)
@staticmethod
def _SwapChainPresentationModeListToStr(presentation_mode_list):
assert len(presentation_mode_list) > 0
list_str = None
for mode in presentation_mode_list:
mode_str = TraceIntegrationTest._SwapChainPresentationModeToStr(mode)
if list_str is not None:
list_str = '%s, %s' % (list_str, mode_str)
else:
list_str = mode_str
return '[%s]' % list_str
@staticmethod
def _DisabledByDefaultTraceCategory(category):
return 'disabled-by-default-%s' % category
#########################################
# The test success evaluation functions
def _EvaluateSuccess_CheckGLCategory(self, category, event_iterator):
for event in event_iterator:
if (event.category == category and
event.args.get('gl_category', None) == 'gpu_toplevel'):
break
else:
self.fail('Trace markers for GPU category %s were not found' % category)
return _TEST_DONE
def _EvaluateSuccess_CheckVideoModeNoOverlay(self, category, event_iterator):
return self._CheckVideoModeHelper(category, event_iterator, no_overlay=True)
def _EvaluateSuccess_CheckVideoMode(self, category, event_iterator):
return self._CheckVideoModeHelper(category, event_iterator,
no_overlay=False)
def _CheckVideoModeHelper(self, category, event_iterator, no_overlay):
os_name = self.browser.platform.GetOSName()
assert os_name and os_name.lower() == 'win'
overlay_bot_config = self.GetOverlayBotConfig()
if overlay_bot_config is None:
self.fail('Overlay bot config can not be determined')
assert overlay_bot_config.get('direct_composition', False)
expected_pixel_format = _SWAP_CHAIN_PIXEL_FORMAT_NV12
expected_presentation_mode = _SWAP_CHAIN_PRESENTATION_MODE_COMPOSED
if overlay_bot_config.get('supports_overlays', False):
supports_yuy2 = False
supports_nv12 = False
if overlay_bot_config.get('overlay_cap_yuy2', 'NONE') != 'NONE':
supports_yuy2 = True
if overlay_bot_config.get('overlay_cap_nv12', 'NONE') != 'NONE':
supports_nv12 = True
assert supports_yuy2 or supports_nv12
if not no_overlay:
expected_presentation_mode = _SWAP_CHAIN_PRESENTATION_MODE_OVERLAY
if not supports_nv12:
expected_pixel_format = _SWAP_CHAIN_PIXEL_FORMAT_YUY2
pixel_format_history = []
presentation_mode_history = []
previous_time = None
invalid_info_encountered = False
for event in event_iterator:
if event.category != category:
continue
if event.name == 'SwapChainFrameInfoInvalid':
error_code = event.args.get('ErrorCode', None)
if error_code is None:
self.fail('ErrorCode is missing from SwapChainFrameInfoInvalid event')
invalid_info_encountered = True
logging.info('Swap chain presentation stats collection failed: %s',
hex(error_code))
continue
if event.name != 'SwapChainFrameInfo':
continue
if previous_time is not None:
# Sanity check that events are chronically sorted
assert previous_time < event.start
pixel_format = event.args.get('SwapChain.PixelFormat', None)
presentation_mode = event.args.get('SwapChain.PresentationMode', None)
if pixel_format is None or presentation_mode is None:
self.fail('PixelFormat or PresentationMode is missing from event')
pixel_format_history.append(pixel_format)
presentation_mode_history.append(presentation_mode)
previous_time = event.start
if len(pixel_format_history) == 0 or len(presentation_mode_history) == 0:
# In theory 'supports_overlays' needs to be true to trigger a fail, but
# all DirectComposition test pages run with commandline switch
# --enable-direct-composition-layers.
if invalid_info_encountered:
return _TEST_REPEAT
self.fail('Trace markers of name SwapChainFrameInfo were not found')
# The last relevant event is selected.
if expected_pixel_format != pixel_format_history[-1]:
self.fail('SwapChain pixel format mismatch, expected %s got %s' %
(TraceIntegrationTest._SwapChainPixelFormatToStr(
expected_pixel_format),
TraceIntegrationTest._SwapChainPixelFormatListToStr(
pixel_format_history)))
if expected_presentation_mode != presentation_mode_history[-1]:
self.fail('SwapChain presentation mode mismatch, expected %s got %s' %
(TraceIntegrationTest._SwapChainPresentationModeToStr(
expected_presentation_mode),
TraceIntegrationTest._SwapChainPresentationModeListToStr(
presentation_mode_history)))
return _TEST_DONE
def load_tests(loader, tests, pattern):
del loader, tests, pattern # Unused.
return gpu_integration_test.LoadAllTestsInModule(sys.modules[__name__])
......@@ -21,3 +21,6 @@ class TraceTestExpectations(GpuTestExpectations):
# context loss which results in hardware decoder loss.
self.Skip('*_Video_Context_Loss_MP4', ['android'], bug=580386)
# Skip on platforms where DirectComposition isn't supported
self.Skip('VideoTraceTest_*',
['mac', 'linux', 'android', 'chromeos', 'win7'], bug=867136)
......@@ -1238,9 +1238,10 @@ bool DCLayerTree::SwapChainPresenter::PresentToSwapChain(
return false;
}
UMA_HISTOGRAM_ENUMERATION(
"GPU.DirectComposition.SwapChainFormat2",
is_yuv_swapchain_ ? g_overlay_format_used : OverlayFormat::kBGRA);
OverlayFormat swap_chain_format =
is_yuv_swapchain_ ? g_overlay_format_used : OverlayFormat::kBGRA;
UMA_HISTOGRAM_ENUMERATION("GPU.DirectComposition.SwapChainFormat2",
swap_chain_format);
frames_since_color_space_change_++;
......@@ -1248,10 +1249,25 @@ bool DCLayerTree::SwapChainPresenter::PresentToSwapChain(
if (SUCCEEDED(swap_chain_.CopyTo(swap_chain_media.GetAddressOf()))) {
DCHECK(swap_chain_media);
DXGI_FRAME_STATISTICS_MEDIA stats = {};
if (SUCCEEDED(swap_chain_media->GetFrameStatisticsMedia(&stats))) {
// GetFrameStatisticsMedia fails with DXGI_ERROR_FRAME_STATISTICS_DISJOINT
// sometimes, which means an event (such as power cycle) interrupted the
// gathering of presentation statistics. In this situation, calling the
// function again succeeds but returns with CompositionMode = NONE.
// Waiting for the DXGI adapter to finish presenting before calling the
// function doesn't get rid of the failure.
HRESULT hr = swap_chain_media->GetFrameStatisticsMedia(&stats);
if (SUCCEEDED(hr)) {
base::UmaHistogramSparse("GPU.DirectComposition.CompositionMode",
stats.CompositionMode);
presentation_history_.AddSample(stats.CompositionMode);
TRACE_EVENT_INSTANT2(TRACE_DISABLED_BY_DEFAULT("gpu.service"),
"SwapChainFrameInfo", TRACE_EVENT_SCOPE_THREAD,
"SwapChain.PresentationMode", stats.CompositionMode,
"SwapChain.PixelFormat", swap_chain_format);
} else {
TRACE_EVENT_INSTANT1(
TRACE_DISABLED_BY_DEFAULT("gpu.service"), "SwapChainFrameInfoInvalid",
TRACE_EVENT_SCOPE_THREAD, "ErrorCode", static_cast<uint32_t>(hr));
}
}
return true;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment