diff --git a/source/tools/webservices/maint_graphics.py b/source/tools/webservices/maint_graphics.py new file mode 100644 index 0000000000..dcc159d4b6 --- /dev/null +++ b/source/tools/webservices/maint_graphics.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +import os + +os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' + +from userreport import maint +maint.collect_graphics() diff --git a/source/tools/webservices/profilemiddleware.py b/source/tools/webservices/profilemiddleware.py new file mode 100644 index 0000000000..39bc8cca2c --- /dev/null +++ b/source/tools/webservices/profilemiddleware.py @@ -0,0 +1,83 @@ +# http://www.no-ack.org/2010/12/yet-another-profiling-middleware-for.html + +import os +import re +import tempfile +from cStringIO import StringIO + +from django.conf import settings +import hotshot +import hotshot.stats + +COMMENT_SYNTAX = ((re.compile(r'^application/(.*\+)?xml|text/html$', re.I), ''), + (re.compile(r'^application/j(avascript|son)$', re.I), '/*', '*/' )) + +class ProfileMiddleware(object): + def process_view(self, request, callback, args, kwargs): + # Create a profile, writing into a temporary file. + filename = tempfile.mktemp() + profile = hotshot.Profile(filename) + + try: + try: + # Profile the call of the view function. + response = profile.runcall(callback, request, *args, **kwargs) + + # If we have got a 3xx status code, further + # action needs to be taken by the user agent + # in order to fulfill the request. So don't + # attach any stats to the content, because of + # the content is supposed to be empty and is + # ignored by the user agent. + if response.status_code // 100 == 3: + return response + + # Detect the appropriate syntax based on the + # Content-Type header. + for regex, begin_comment, end_comment in COMMENT_SYNTAX: + if regex.match(response['Content-Type'].split(';')[0].strip()): + break + else: + # If the given Content-Type is not + # supported, don't attach any stats to + # the content and return the unchanged + # response. + return response + + # The response can hold an iterator, that + # is executed when the content property + # is accessed. So we also have to profile + # the call of the content property. + content = profile.runcall(response.__class__.content.fget, response) + finally: + profile.close() + + # Load the stats from the temporary file and + # write them in a human readable format, + # respecting some optional settings into a + # StringIO object. + stats = hotshot.stats.load(filename) + if getattr(settings, 'PROFILE_MIDDLEWARE_STRIP_DIRS', False): + stats.strip_dirs() + if getattr(settings, 'PROFILE_MIDDLEWARE_SORT', None): + stats.sort_stats(*settings.PROFILE_MIDDLEWARE_SORT) + stats.stream = StringIO() + stats.print_stats(*getattr(settings, 'PROFILE_MIDDLEWARE_RESTRICTIONS', [])) + finally: + os.unlink(filename) + + # Construct an HTML/XML or Javascript comment, with + # the formatted stats, written to the StringIO object + # and attach it to the content of the response. + comment = '\n%s\n\n%s\n\n%s\n' % (begin_comment, stats.stream.getvalue().strip(), end_comment) + response.content = content + comment + + # If the Content-Length header is given, add the + # number of bytes we have added to it. If the + # Content-Length header is ommited or incorrect, + # it remains so in order to don't change the + # behaviour of the web server or user agent. + if response.has_header('Content-Length'): + response['Content-Length'] = int(response['Content-Length']) + len(comment) + + return response diff --git a/source/tools/webservices/settings.py b/source/tools/webservices/settings.py index 95d74b597f..ec7fa84c61 100644 --- a/source/tools/webservices/settings.py +++ b/source/tools/webservices/settings.py @@ -73,8 +73,11 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', +# 'profilemiddleware.ProfileMiddleware', ) +PROFILE_MIDDLEWARE_SORT = ('time', 'calls') + ROOT_URLCONF = 'urls' TEMPLATE_DIRS = ( @@ -101,20 +104,21 @@ INSTALLED_APPS = ( # the site admins on every HTTP 500 error. # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. -#LOGGING = { -# 'version': 1, -# 'disable_existing_loggers': False, -# 'handlers': { -# 'mail_admins': { -# 'level': 'ERROR', -# 'class': 'django.utils.log.AdminEmailHandler' -# } -# }, -# 'loggers': { -# 'django.request':{ -# 'handlers': ['mail_admins'], -# 'level': 'ERROR', -# 'propagate': True, -# }, -# } -#} +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + #'include_html': False, # TODO: use once 1.3 final is released + } + }, + 'loggers': { + 'django.request':{ + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} diff --git a/source/tools/webservices/userreport/admin.py b/source/tools/webservices/userreport/admin.py index ef011ec446..e387872cd9 100644 --- a/source/tools/webservices/userreport/admin.py +++ b/source/tools/webservices/userreport/admin.py @@ -1,4 +1,4 @@ -from userreport.models import UserReport +from userreport.models import UserReport, GraphicsDevice, GraphicsExtension, GraphicsLimit from django.contrib import admin class UserReportAdmin(admin.ModelAdmin): @@ -11,7 +11,20 @@ class UserReportAdmin(admin.ModelAdmin): ] list_display = ('uploader', 'user_id_hash', 'data_type', 'data_version', 'upload_date', 'generation_date') list_filter = ['upload_date', 'generation_date', 'data_type'] - search_fields = ['=uploader', '=user_id_hash'] + search_fields = ['=uploader', '=user_id_hash', 'data'] date_hierarchy = 'upload_date' +class GraphicsDeviceAdmin(admin.ModelAdmin): + pass + +class GraphicsExtensionAdmin(admin.ModelAdmin): + pass + +class GraphicsLimitAdmin(admin.ModelAdmin): + pass + admin.site.register(UserReport, UserReportAdmin) +admin.site.register(GraphicsDevice, GraphicsDeviceAdmin) +admin.site.register(GraphicsExtension, GraphicsExtensionAdmin) +admin.site.register(GraphicsLimit, GraphicsLimitAdmin) + diff --git a/source/tools/webservices/userreport/maint.py b/source/tools/webservices/userreport/maint.py new file mode 100644 index 0000000000..ca87158307 --- /dev/null +++ b/source/tools/webservices/userreport/maint.py @@ -0,0 +1,84 @@ +from userreport.models import UserReport_hwdetect, GraphicsDevice, GraphicsExtension, GraphicsLimit +from django.db import connection, transaction + +def collect_graphics(): + reports = UserReport_hwdetect.objects.filter(data_type = 'hwdetect', data_version__gte = 3) + + print "Gathering data" + count = 0 + + devices = {} + for report in reports: + device = report.gl_device_identifier() + vendor = report.gl_vendor() + renderer = report.gl_renderer() + os = report.os() + driver = report.gl_driver() + exts = report.gl_extensions() + limits = report.gl_limits() + report.clear_cache() + + devices.setdefault( + (device, vendor, renderer, os, driver, exts, tuple(sorted(limits.items()))), + set() + ).add(report.user_id_hash) + + count += 1 + if count % 100 == 0: + print "%d / %d..." % (count, len(reports)) + + print "Saving data" + count = 0 + + cursor = connection.cursor() + + # To get atomic behaviour, construct new tables and then rename them into place at the end + try: + cursor.execute('DROP TABLE IF EXISTS userreport_graphicsdevice_temp, userreport_graphicsextension_temp, userreport_graphicslimit_temp') + except Warning: + pass # ignore harmless warnings when tables don't exist + cursor.execute('CREATE TABLE userreport_graphicsdevice_temp LIKE userreport_graphicsdevice') + cursor.execute('CREATE TABLE userreport_graphicsextension_temp LIKE userreport_graphicsextension') + cursor.execute('CREATE TABLE userreport_graphicslimit_temp LIKE userreport_graphicslimit') + + for (device, vendor, renderer, os, driver, exts, limits), users in devices.items(): + cursor.execute(''' + INSERT INTO userreport_graphicsdevice_temp (id, device_name, vendor, renderer, os, driver, usercount) + VALUES (NULL, %s, %s, %s, %s, %s, %s) + ''', (device, vendor, renderer, os, driver, len(users))) + + device_id = cursor.lastrowid + + if len(exts): + ext_placeholders = ','.join('(NULL, %s, %s)' for ext in exts) + ext_values = sum(([device_id, ext] for ext in exts), []) + cursor.execute('INSERT INTO userreport_graphicsextension_temp (id, device_id, name) VALUES %s' % ext_placeholders, ext_values) + + if len(limits): + limit_placeholders = ','.join('(NULL, %s, %s, %s)' for limit in limits) + limit_values = sum(([device_id, name, str(value)] for name,value in limits), []) + cursor.execute('INSERT INTO userreport_graphicslimit_temp (id, device_id, name, value) VALUES %s' % limit_placeholders, limit_values) + + count += 1 + if count % 100 == 0: + print "%d / %d..." % (count, len(devices)) + + cursor.execute(''' + RENAME TABLE + userreport_graphicsdevice TO userreport_graphicsdevice_old, + userreport_graphicsextension TO userreport_graphicsextension_old, + userreport_graphicslimit TO userreport_graphicslimit_old, + userreport_graphicsdevice_temp TO userreport_graphicsdevice, + userreport_graphicsextension_temp TO userreport_graphicsextension, + userreport_graphicslimit_temp TO userreport_graphicslimit + ''') + cursor.execute(''' + DROP TABLE IF EXISTS + userreport_graphicsdevice_old, + userreport_graphicsextension_old, + userreport_graphicslimit_old + ''') + + transaction.commit_unless_managed() + + print "Finished" diff --git a/source/tools/webservices/userreport/models.py b/source/tools/webservices/userreport/models.py index e5231d066f..9a5f261dcd 100644 --- a/source/tools/webservices/userreport/models.py +++ b/source/tools/webservices/userreport/models.py @@ -33,6 +33,15 @@ class UserReport(models.Model): self.cached_json = None return self.cached_json + def data_json_nocache(self): + try: + return simplejson.loads(self.data) + except: + return None + + def clear_cache(self): + delattr(self, 'cached_json') + def downcast(self): if self.data_type == 'hwdetect': return UserReport_hwdetect.objects.get(id = self.id) @@ -62,15 +71,16 @@ class UserReport_hwdetect(UserReport): return None # The renderer string should typically be interpreted as UTF-8 try: - return json['GL_RENDERER'].encode('iso-8859-1').decode('utf-8') + return json['GL_RENDERER'].encode('iso-8859-1').decode('utf-8').strip() except UnicodeError: - return json['GL_RENDERER'] + return json['GL_RENDERER'].strip() def gl_extensions(self): json = self.data_json() if json is None or 'GL_EXTENSIONS' not in json: return None - return frozenset(json['GL_EXTENSIONS'].strip().split(' ')) + vals = re.split(r'\s+', json['GL_EXTENSIONS']) + return frozenset(v for v in vals if len(v)) # skip empty strings (e.g. no extensions at all, or leading/trailing space) def gl_limits(self): json = self.data_json() @@ -91,6 +101,10 @@ class UserReport_hwdetect(UserReport): # Hide some values that got deleted from the report in r8953, for consistency if k in ('GL_MAX_COLOR_MATRIX_STACK_DEPTH', 'GL_FRAGMENT_PROGRAM_ARB.GL_MAX_PROGRAM_ADDRESS_REGISTERS_ARB', 'GL_FRAGMENT_PROGRAM_ARB.GL_MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB'): continue + # Hide some pixel depth values that are not really correlated with device + if k in ('GL_RED_BITS', 'GL_GREEN_BITS', 'GL_BLUE_BITS', 'GL_ALPHA_BITS', 'GL_INDEX_BITS', 'GL_DEPTH_BITS', 'GL_STENCIL_BITS', + 'GL_ACCUM_RED_BITS', 'GL_ACCUM_GREEN_BITS', 'GL_ACCUM_BLUE_BITS', 'GL_ACCUM_ALPHA_BITS'): + continue limits[k] = v return limits @@ -98,17 +112,26 @@ class UserReport_hwdetect(UserReport): # (skipping boring hardware/driver details) def gl_device_identifier(self): r = self.gl_renderer() - m = re.match(r'^(?:ATI |Mesa DRI )?(.*?)\s*(?:GEM 20100330 DEVELOPMENT|GEM 20091221 2009Q4|20090101|Series)?\s*(?:x86|/AGP|/PCI|/MMX|/MMX\+|/SSE|/SSE2|/3DNOW!|/3DNow!\+)*(?: TCL| NO-TCL)?(?: DRI2)?$', r) + m = re.match(r'^(?:AMD |ATI |NVIDIA |Mesa DRI )?(.*?)\s*(?:GEM 20100328 2010Q1|GEM 20100330 DEVELOPMENT|GEM 20091221 2009Q4|20090101|Series)?\s*(?:x86|/AGP|/PCI|/MMX|/MMX\+|/SSE|/SSE2|/3DNOW!|/3DNow!|/3DNow!\+)*(?: TCL| NO-TCL)?(?: DRI2)?(?: \(Microsoft Corporation - WDDM\))?(?: OpenGL Engine)?\s*$', r) if m: r = m.group(1) - return r + return r.strip() + + def gl_vendor(self): + json = self.data_json() + return json['GL_VENDOR'].strip() # Construct a nice string identifying the driver def gl_driver(self): json = self.data_json() gfx_drv_ver = json['gfx_drv_ver'] - # Try the Mesa style first + # Try the Mesa git style first + m = re.match(r'^OpenGL \d+\.\d+(?:\.\d+)? (Mesa \d+\.\d+)-devel \(git-([a-f0-9]+)', gfx_drv_ver) + if m: + return '%s-git-%s' % (m.group(1), m.group(2)) + + # Try the normal Mesa style m = re.match(r'^OpenGL \d+\.\d+(?:\.\d+)? (Mesa .*)$', gfx_drv_ver) if m: return m.group(1) @@ -119,10 +142,15 @@ class UserReport_hwdetect(UserReport): return m.group(1) # Try the ATI Catalyst Linux style - m = re.match(r'^OpenGL (\d+\.\d+\.\d+) Compatibility Profile Context$', gfx_drv_ver) + m = re.match(r'^OpenGL (\d+\.\d+\.\d+) Compatibility Profile Context(?: FireGL)?$', gfx_drv_ver) if m: return m.group(1) + # Try the non-direct-rendering ATI Catalyst Linux style + m = re.match(r'^OpenGL 1\.4 \((\d+\.\d+\.\d+) Compatibility Profile Context(?: FireGL)?\)$', gfx_drv_ver) + if m: + return '%s (indirect)' % m.group(1) + # Try to guess the relevant Windows driver # (These are the ones listed in lib/sysdep/os/win/wgfx.cpp) @@ -138,6 +166,10 @@ class UserReport_hwdetect(UserReport): if json['GL_VENDOR'] in ('ATI Technologies Inc.', 'Advanced Micro Devices, Inc.'): m = re.search(r'atioglxx.dll \((.*?)\)', gfx_drv_ver) if m: return m.group(1) + m = re.search(r'atioglx2.dll \((.*?)\)', gfx_drv_ver) + if m: return m.group(1) + m = re.search(r'atioglaa.dll \((.*?)\)', gfx_drv_ver) + if m: return m.group(1) if json['GL_VENDOR'] == 'Intel': # Assume 64-bit takes precedence @@ -148,5 +180,28 @@ class UserReport_hwdetect(UserReport): # Legacy 32-bit m = re.search(r'iglicd32.dll \((.*?)\)', gfx_drv_ver) if m: return m.group(1) + m = re.search(r'ialmgicd32.dll \((.*?)\)', gfx_drv_ver) + if m: return m.group(1) + m = re.search(r'ialmgicd.dll \((.*?)\)', gfx_drv_ver) + if m: return m.group(1) return gfx_drv_ver + + +class GraphicsDevice(models.Model): + device_name = models.CharField(max_length = 128, db_index = True) + vendor = models.CharField(max_length = 64) + renderer = models.CharField(max_length = 128) + os = models.CharField(max_length = 16) + driver = models.CharField(max_length = 128) + usercount = models.IntegerField() + +class GraphicsExtension(models.Model): + device = models.ForeignKey(GraphicsDevice) + name = models.CharField(max_length = 128, db_index = True) + +class GraphicsLimit(models.Model): + device = models.ForeignKey(GraphicsDevice) + name = models.CharField(max_length = 128, db_index = True) + value = models.CharField(max_length = 64) + diff --git a/source/tools/webservices/userreport/templates/jsonformat.html b/source/tools/webservices/userreport/templates/jsonformat.html new file mode 100644 index 0000000000..5983269d99 --- /dev/null +++ b/source/tools/webservices/userreport/templates/jsonformat.html @@ -0,0 +1,99 @@ + + +
The JSON data currently +has the format shown below.
+ +Each entry in the outer array is a single distinct set of device features
+(array of "extensions" plus hash of implementation-dependent "limits").
Each feature set is associated with an array of "devices".
+For each of those devices, we have received a report containing that particular feature set;
+this is effectively just a compression mechanism so we don't have to duplicate the entire
+feature set description when dozens of devices have identical features.
Each device has an "os" ("Windows", "Linux", "OS X"),
+a "renderer" (from GL_RENDERER),
+a "vendor" (from GL_VENDOR),
+and a "driver" (typically derived from the appropriate DLL on Windows,
+or sometimes a list of lots of DLLs if we can't figure out which is appropriate,
+or derived from the full GL_VERSION string on Linux).
+[
+ {
+ "devices": [
+ {
+ "driver": "6.14.10.8494",
+ "os": "Windows",
+ "renderer": "AMD 760G",
+ "vendor": "ATI Technologies Inc."
+ }
+ ],
+ "extensions": [
+ "GL_AMD_performance_monitor",
+ ...
+ "WGL_EXT_swap_control"
+ ],
+ "limits": {
+ "GL_ALIASED_LINE_WIDTH_RANGE[0]": "1",
+ ...
+ "GL_VERTEX_PROGRAM_ARB.GL_MAX_PROGRAM_TEMPORARIES_ARB": "160"
+ }
+ },
+ {
+ "devices": [
+ {
+ "driver": "6.14.10.10057",
+ "os": "Windows",
+ "renderer": "AMD M880G with ATI Mobility Radeon HD 4200",
+ "vendor": "ATI Technologies Inc."
+ },
+ {
+ "driver": "6.14.10.10179",
+ "os": "Windows",
+ "renderer": "AMD M880G with ATI Mobility Radeon HD 4250",
+ "vendor": "ATI Technologies Inc."
+ },
+ {
+ "driver": "3.3.10188",
+ "os": "Linux",
+ "renderer": "ATI Mobility Radeon HD 3400 Series",
+ "vendor": "ATI Technologies Inc."
+ },
+ {
+ "driver": "6.14.10.10151",
+ "os": "Windows",
+ "renderer": "ATI Mobility Radeon HD 3400 Series",
+ "vendor": "ATI Technologies Inc."
+ },
+ ...
+ ],
+ "extensions": [
+ "GL_AMDX_debug_output",
+ ...
+ "WGL_EXT_swap_control"
+ ],
+ "limits": {
+ "GL_ALIASED_LINE_WIDTH_RANGE[0]": "1",
+ ...
+ "GL_VERTEX_PROGRAM_ARB.GL_MAX_PROGRAM_TEMPORARIES_ARB": "160"
+ }
+ },
+ ...
+}
+
+
diff --git a/source/tools/webservices/userreport/templates/reports/opengl_device.html b/source/tools/webservices/userreport/templates/reports/opengl_device.html
index 1dbe09398d..6e54a29e1a 100644
--- a/source/tools/webservices/userreport/templates/reports/opengl_device.html
+++ b/source/tools/webservices/userreport/templates/reports/opengl_device.html
@@ -8,11 +8,19 @@ td.true { background: #3f3; }
td.false { background: #f33; }
td.true.alt { background: #2e2; }
td.false.alt { background: #e22; }
+td.changed { font-weight: bold; }
-.device-status ul {
+.devices {
+ vertical-align: top;
+}
+
+.devices ul {
margin: 0;
padding: 0;
- padding-left: 2em;
+ list-style-type: none;
+ font-weight: normal;
+ font-size: smaller;
+ white-space: pre;
}
{% endblock %}
@@ -28,26 +36,30 @@ OpenGL capabilities report
-The table here shows the features reported for the following devices:
+The table here shows the features reported for devices with the following GL_RENDERER strings:
Different driver versions may have different feature sets, and we may have conflicting reports from the same driver version. -There is a column for each distinct set of reported features.
+There is a column for each distinct set of reported features. +The column heading gives the short device name, and the set of +driver versions with that feature set. + +Green cells indicate supported extensions; red cells indicate non-supported extensions. +Cells are marked with bold when their value differs from the previous cell in the same row.
-Green cells indicate supported extensions; red cells indicate non-supported extensions.
| {% for ext in all_exts %} - {% if not forloop.counter0|mod:30 %} | ||
| {% for device in devices %} - | + |
{{ device.0.device }} ({{ device.0.os }}):
|
|---|---|---|
| {% for device in devices %} - | + |
{{ device.0.device }} ({{ device.0.os }}):
|
| {{ device.2.0|dictget:limit }} + | {{ device.2.0|dictget:limit }} {% endfor %} {% endfor %} |
Supported by:+ | Supported by + {{ usercounts.true }} user{{ usercounts.true|pluralize }}: | ||||||
| Vendor | Renderer + | Users | OS | Driver versions {% for device in values.true|sorteddeviceitems %} - | |||
|---|---|---|---|---|---|---|---|
| {{ device.0.vendor }} | {{ device.0.renderer }} + | {{ device.1.usercount }} | {{ device.0.os }} |
| |||
Not supported by:+ | Not supported by + {{ usercounts.false }} user{{ usercounts.false|pluralize }}: | ||||||
| Vendor | Renderer + | Users | OS | Driver versions {% for device in values.false|sorteddeviceitems %} - | |||
| {{ device.0.vendor }} | {{ device.0.renderer }} + | {{ device.1.usercount }} | {{ device.0.os }} |
| |||
| Value + | Number of users + {% for val in values.keys|sortreversed %} + |
|---|---|
| {{ val|default_if_none:"Unsupported/unknown" }} + | {{ usercounts|dictget:val }} ({% widthratio usercounts|dictget:val num_users 100 %}%) + {% endfor %} + |
Value: {{ val|default_if_none:"Unsupported/unknown" }}+ | Value "{{ val|default_if_none:"Unsupported/unknown" }}" + ({{ usercounts|dictget:val }} user{{ usercounts|dictget:val|pluralize }}): | ||||||
| Vendor | Renderer + | Users | OS | Driver versions @@ -106,9 +145,10 @@ OpenGL capabilities report: {{ feature }} | |||
|---|---|---|---|---|---|---|---|
| {{ device.0.vendor }} | {{ device.0.renderer }} + | {{ device.1.usercount }} | {{ device.0.os }} |
Based on data submitted by players of 0 A.D. - Browse the data here, or download as JSON. +See the index page for more stuff. +Contact Philip Taylor for questions. + +Browse the data here, or download as JSON +(see format description). Feel free to do whatever you want with the data. -See the index page for more stuff. The listed extensions are based on the GL_EXTENSIONS string: we don't show an extension as supported if it's not explicitly advertised, even if GL_VERSION is a version where that extension was folded into the main spec. +The extension support percentages are based on the number of user/device/driver combinations, +from a total of {{ num_users }}. This is obviously hopelessly biased and unrepresentative +so don't read too much into the numbers. + +The listed device names are based on GL_RENDERER, with boring components stripped out. +Driver versions on Windows are determined from DLL versions; if we can't guess which is the +correct DLL then we try to list all the detectable DLLs. Driver versions on Linux are determined +from GL_VERSION, when it's encoded in there. + +Extension supportSort by @@ -80,18 +99,20 @@ Sort by name. +Implementation limits
Device details
| |||