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 @@ + + +0 A.D. report service + + +

(Back to index page.)

+ +

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

(Back to index page.)

-

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 driver in device.1 %}
  • {{ driver }}{% endfor %}
{% endfor %} @@ -64,7 +76,7 @@ There is a column for each distinct set of reported features.

{% if not forloop.counter|mod:30 %}
{% for device in devices %} - + {{ device.0.device }} ({{ device.0.os }}):
    {% for driver in device.1 %}
  • {{ driver }}{% endfor %}
{% endfor %} @@ -73,7 +85,7 @@ There is a column for each distinct set of reported features.

{{ limit|prettify_gl_title }} {% for device in devices %} - {{ device.2.0|dictget:limit }} + {{ device.2.0|dictget:limit }} {% endfor %} {% endfor %}
diff --git a/source/tools/webservices/userreport/templates/reports/opengl_feature.html b/source/tools/webservices/userreport/templates/reports/opengl_feature.html index c553b52b02..bc90c22d84 100644 --- a/source/tools/webservices/userreport/templates/reports/opengl_feature.html +++ b/source/tools/webservices/userreport/templates/reports/opengl_feature.html @@ -24,6 +24,22 @@ tr.head { background: #ddd; } +tr.true { + background: #f6fff6; +} + +tr.true.alt { + background: #f0fff0; +} + +tr.false { + background: #fff6f6; +} + +tr.false.alt { + background: #fff0f0; +} + tr.data:hover { background: #ddd; } @@ -47,58 +63,81 @@ OpenGL capabilities report: {{ feature }} +{% if values.true %} - + - +

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 }}
    -{% for driver in device.1|sort %}
  • {{ driver }}{% endfor %} +{% for driver in device.1.drivers|sort %}
  • {{ driver }}{% endfor %}
{% endfor %} +{% endif %} +{% if values.false %}

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 }}
    -{% for driver in device.1|sort %}
  • {{ driver }}{% endfor %} +{% for driver in device.1.drivers|sort %}
  • {{ driver }}{% endfor %}
{% endfor %} +{% endif %}
{% else %} + + + +
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 %} +
+ {% for val in values.keys|sortreversed %} -

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 }}
    -{% for driver in device.1|sort %}
  • {{ driver }}{% endfor %} +{% for driver in device.1.drivers|sort %}
  • {{ driver }}{% endfor %}
{% endfor %} diff --git a/source/tools/webservices/userreport/templates/reports/opengl_index.html b/source/tools/webservices/userreport/templates/reports/opengl_index.html index 2ea33d64c8..1340077c31 100644 --- a/source/tools/webservices/userreport/templates/reports/opengl_index.html +++ b/source/tools/webservices/userreport/templates/reports/opengl_index.html @@ -27,6 +27,12 @@ li:hover { background: #ddd; } +ul { + list-style-type: none; + margin: 2px 10px 2px 0; + padding: 0; +} + {% endblock %} {% block title %} @@ -64,15 +70,28 @@ function sort_by_name(ul)

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 support

Sort by @@ -80,18 +99,20 @@ Sort by name.
    {% for ext in all_exts %} -
  • -{% widthratio ext_devices|dictget:ext|length all_devices|length 100 %}% -(spec) +
  • +{% widthratio ext_devices|dictget:ext num_users 100 %}% +(s) {{ ext }} {% if ext in ext_versions %}(~GL{{ ext_versions|dictget:ext }}){% endif %} {% endfor %}
+
+

Implementation limits

@@ -100,7 +121,7 @@ Sort by

Device details

    {% for device,users in all_devices|sorteditems %} -
  • {{ device }} {% if 0 %}({{ users|length }} users){% endif %} +
  • {{ device }} {% if 0 %}({{ users }} users){% endif %} {% endfor %}
diff --git a/source/tools/webservices/userreport/templatetags/report_tags.py b/source/tools/webservices/userreport/templatetags/report_tags.py index f446bfda27..3369a5b152 100644 --- a/source/tools/webservices/userreport/templatetags/report_tags.py +++ b/source/tools/webservices/userreport/templatetags/report_tags.py @@ -37,15 +37,18 @@ def prettify_json(value): @stringfilter def glext_spec_link(value): c = value.split('_', 2) + if len(c) < 2: return '' return 'http://www.opengl.org/registry/specs/%s/%s.txt' % (c[1], c[2]) @register.filter @stringfilter def prettify_gl_title(value): + if value[-4:] in ('_ARB', '_EXT'): + value = value[:-4] if value.startswith('GL_FRAGMENT_PROGRAM_ARB.'): - return value[24:] + ' (fragment)' + value = value[24:] + ' (fragment)' if value.startswith('GL_VERTEX_PROGRAM_ARB.'): - return value[22:] + ' (vertex)' + value = value[22:] + ' (vertex)' return value @register.filter diff --git a/source/tools/webservices/userreport/urls.py b/source/tools/webservices/userreport/urls.py index f51371ac2a..8dc5c170ba 100644 --- a/source/tools/webservices/userreport/urls.py +++ b/source/tools/webservices/userreport/urls.py @@ -5,9 +5,12 @@ urlpatterns = patterns('', (r'^opengl/$', 'userreport.views.report_opengl_index'), (r'^opengl/json$', 'userreport.views.report_opengl_json'), + (r'^opengl/json/format$', 'userreport.views.report_opengl_json_format'), (r'^opengl/feature/(?P[^/]+)$', 'userreport.views.report_opengl_feature'), (r'^opengl/device/(?P.+)$', 'userreport.views.report_opengl_device'), (r'^opengl/device', 'userreport.views.report_opengl_device_compare'), (r'^cpu/$', 'userreport.views.report_cpu'), + + (r'^usercount/$', 'userreport.views.report_usercount'), ) diff --git a/source/tools/webservices/userreport/urls_private.py b/source/tools/webservices/userreport/urls_private.py index 3d18507e4d..4ca44d66f2 100644 --- a/source/tools/webservices/userreport/urls_private.py +++ b/source/tools/webservices/userreport/urls_private.py @@ -2,9 +2,12 @@ from django.conf.urls.defaults import * urlpatterns = patterns('', (r'^hwdetect/$', 'userreport.views_private.report_hwdetect'), + (r'^hwdetect_test_data/$', 'userreport.views_private.report_hwdetect_test_data'), (r'^messages/$', 'userreport.views_private.report_messages'), (r'^profile/$', 'userreport.views_private.report_profile'), (r'^performance/$', 'userreport.views_private.report_performance'), (r'^gfx/$', 'userreport.views_private.report_gfx'), + (r'^ram/$', 'userreport.views_private.report_ram'), + (r'^os/$', 'userreport.views_private.report_os'), (r'^user/([0-9a-f]+)$', 'userreport.views_private.report_user'), ) diff --git a/source/tools/webservices/userreport/views.py b/source/tools/webservices/userreport/views.py index 28a3b07f63..d428c34ecf 100644 --- a/source/tools/webservices/userreport/views.py +++ b/source/tools/webservices/userreport/views.py @@ -1,15 +1,17 @@ -from userreport.models import UserReport, UserReport_hwdetect +from userreport.models import UserReport, UserReport_hwdetect, GraphicsDevice, GraphicsExtension, GraphicsLimit import userreport.x86 as x86 import userreport.gl import hashlib import datetime import zlib +import re from django.http import HttpResponseBadRequest, HttpResponse, Http404, QueryDict from django.shortcuts import render_to_response from django.template import RequestContext from django.utils import simplejson +from django.db import connection, transaction from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST @@ -67,13 +69,14 @@ def index(request): def report_cpu(request): reports = UserReport_hwdetect.objects - reports = reports.filter(data_type = 'hwdetect', data_version__gte = 4) + reports = reports.filter(data_type = 'hwdetect', data_version__gte = 4, data_version__lte = 5) + # TODO: add v6 support all_users = set() cpus = {} for report in reports: - json = report.data_json() + json = report.data_json_nocache() if json is None: continue @@ -140,8 +143,11 @@ def report_cpu(request): icaches.pop(0) return tuple(caches) - cpu['caches'] = fmt_caches(json['x86_dcaches'], json['x86_icaches'], fmt_cache) - cpu['tlbs'] = fmt_caches(json['x86_dtlbs'], json['x86_itlbs'], fmt_tlb) + try: + cpu['caches'] = fmt_caches(json['x86_dcaches'], json['x86_icaches'], fmt_cache) + cpu['tlbs'] = fmt_caches(json['x86_dtlbs'], json['x86_itlbs'], fmt_tlb) + except TypeError: + continue # skip on bogus cache data caps = set() for (n,_,b) in x86.cap_bits: @@ -156,61 +162,67 @@ def report_cpu(request): return render_to_response('reports/cpu.html', {'cpus': cpus, 'x86_cap_descs': x86.cap_descs}) -def get_hwdetect_reports(): - reports = UserReport_hwdetect.objects - reports = reports.filter(data_type = 'hwdetect', data_version__gte = 3) - return reports - def report_opengl_json(request): - reports = get_hwdetect_reports() - devices = {} + reports = GraphicsDevice.objects.all() for report in reports: - json = report.data_json() - if json is None: - continue + exts = frozenset(e.name for e in report.graphicsextension_set.all()) + limits = dict((l.name, l.value) for l in report.graphicslimit_set.all()) - exts = report.gl_extensions() - limits = report.gl_limits() + device = (report.vendor, report.renderer, report.os, report.driver) + devices.setdefault((hashabledict(limits), exts), set()).add(device) - devices.setdefault(hashabledict({'renderer': report.gl_renderer(), 'os': report.os()}), {}).setdefault((hashabledict(limits), exts), set()).add(report.gl_driver()) - - distinct_devices = [] - for (renderer, v) in devices.items(): - for (caps, versions) in v.items(): - distinct_devices.append((renderer, sorted(versions), caps)) - distinct_devices.sort(key = lambda x: (x[0]['renderer'], x[0]['os'], x)) + sorted_devices = sorted(devices.items(), key = lambda (k,deviceset): sorted(deviceset)) data = [] - for r,vs,(limits,exts) in distinct_devices: - data.append({'device': r, 'drivers': vs, 'limits': limits, 'extensions': sorted(exts)}) + for (limits,exts),deviceset in sorted_devices: + devices = [ + {'vendor': v, 'renderer': r, 'os': o, 'driver': d} + for (v,r,o,d) in sorted(deviceset) + ] + data.append({'devices': devices, 'limits': limits, 'extensions': sorted(exts)}) json = simplejson.dumps(data, indent=1, sort_keys=True) return HttpResponse(json, content_type = 'text/plain') +def report_opengl_json_format(request): + return render_to_response('jsonformat.html') + def report_opengl_index(request): - reports = get_hwdetect_reports() + cursor = connection.cursor() - all_limits = set() - all_exts = set() - all_devices = {} - ext_devices = {} + cursor.execute(''' + SELECT SUM(usercount) + FROM userreport_graphicsdevice + ''') + num_users = sum(c for (c,) in cursor) - for report in reports: - json = report.data_json() - if json is None: - continue + cursor.execute(''' + SELECT name, SUM(usercount) + FROM userreport_graphicsextension e + JOIN userreport_graphicsdevice d + ON e.device_id = d.id + GROUP BY name + ''') + exts = cursor.fetchall() + all_exts = set(n for n,c in exts) + ext_devices = dict((n,c) for n,c in exts) - device = report.gl_device_identifier() - all_devices.setdefault(device, set()).add(report.user_id_hash) + cursor.execute(''' + SELECT name + FROM userreport_graphicslimit l + JOIN userreport_graphicsdevice d + ON l.device_id = d.id + GROUP BY name + ''') + all_limits = set(n for (n,) in cursor.fetchall()) - exts = report.gl_extensions() - all_exts |= exts - for ext in exts: - ext_devices.setdefault(ext, set()).add(device) - - limits = report.gl_limits() - all_limits |= set(limits.keys()) + cursor.execute(''' + SELECT device_name, SUM(usercount) + FROM userreport_graphicsdevice + GROUP BY device_name + ''') + all_devices = dict((n,c) for n,c in cursor.fetchall()) all_limits = sorted(all_limits) all_exts = sorted(all_exts) @@ -220,76 +232,107 @@ def report_opengl_index(request): 'all_exts': all_exts, 'all_devices': all_devices, 'ext_devices': ext_devices, + 'num_users': num_users, 'ext_versions': userreport.gl.glext_versions, }) def report_opengl_feature(request, feature): - reports = get_hwdetect_reports() - all_values = set() + usercounts = {} values = {} + + cursor = connection.cursor() + is_extension = False - - for report in reports: - json = report.data_json() - if json is None: - continue - - device = report.gl_device_identifier() - exts = report.gl_extensions() - limits = hashabledict(report.gl_limits()) - - val = None - if feature in exts: - val = True - is_extension = True - elif feature in limits: - val = limits[feature] - all_values.add(val) - - values.setdefault(val, {}).setdefault(hashabledict({'vendor': json['GL_VENDOR'], 'renderer': report.gl_renderer(), 'os': report.os(), 'device': device}), set()).add(report.gl_driver()) - - if values.keys() == [None]: - raise Http404 + if re.search(r'[a-z]', feature): + is_extension = True if is_extension: - values = { - 'true': values.get(True, {}), - 'false': values.get(None, {}), - } + cursor.execute(''' + SELECT vendor, renderer, os, driver, device_name, SUM(usercount), + (SELECT 1 FROM userreport_graphicsextension e WHERE e.name = %s AND e.device_id = d.id) AS val + FROM userreport_graphicsdevice d + GROUP BY vendor, renderer, os, driver, device_name, val + ''', [feature]) + + for vendor, renderer, os, driver, device_name, usercount, val in cursor: + val = 'true' if val else 'false' + all_values.add(val) + usercounts[val] = usercounts.get(val, 0) + usercount + v = values.setdefault(val, {}).setdefault(hashabledict({ + 'vendor': vendor, + 'renderer': renderer, + 'os': os, + 'device': device_name + }), {'usercount': 0, 'drivers': set()}) + v['usercount'] += usercount + v['drivers'].add(driver) + + else: + cursor.execute(''' + SELECT value, vendor, renderer, os, driver, device_name, usercount + FROM userreport_graphicslimit l + JOIN userreport_graphicsdevice d + ON l.device_id = d.id + WHERE name = %s + ''', [feature]) + + for val, vendor, renderer, os, driver, device_name, usercount in cursor: + # Convert to int/float if possible, for better sorting + try: val = int(val) + except ValueError: + try: val = float(val) + except ValueError: pass + + all_values.add(val) + usercounts[val] = usercounts.get(val, 0) + usercount + v = values.setdefault(val, {}).setdefault(hashabledict({ + 'vendor': vendor, + 'renderer': renderer, + 'os': os, + 'device': device_name + }), {'usercount': 0, 'drivers': set()}) + v['usercount'] += usercount + v['drivers'].add(driver) + + if values.keys() == [] or values.keys() == ['false']: + raise Http404 + + num_users = sum(usercounts.values()) return render_to_response('reports/opengl_feature.html', { 'feature': feature, 'all_values': all_values, 'values': values, 'is_extension': is_extension, + 'usercounts': usercounts, + 'num_users': num_users, }) def report_opengl_devices(request, selected): - reports = get_hwdetect_reports() + cursor = connection.cursor() + cursor.execute(''' + SELECT DISTINCT device_name + FROM userreport_graphicsdevice + ''') + all_devices = set(n for (n,) in cursor.fetchall()) all_limits = set() all_exts = set() - all_devices = set() devices = {} + gl_renderers = set() + reports = GraphicsDevice.objects.filter(device_name__in = selected) for report in reports: - json = report.data_json() - if json is None: - continue - - device = report.gl_device_identifier() - all_devices.add(device) - if device not in selected: - continue - - exts = report.gl_extensions() + exts = frozenset(e.name for e in report.graphicsextension_set.all()) all_exts |= exts - limits = report.gl_limits() + limits = dict((l.name, l.value) for l in report.graphicslimit_set.all()) all_limits |= set(limits.keys()) - devices.setdefault(hashabledict({'device': report.gl_device_identifier(), 'os': report.os()}), {}).setdefault((hashabledict(limits), exts), set()).add(report.gl_driver()) + devices.setdefault(hashabledict({'device': report.device_name, 'os': report.os}), {}).setdefault((hashabledict(limits), exts), set()).add(report.driver) + + gl_renderers.add(report.renderer) if len(selected) == 1 and len(devices) == 0: raise Http404 @@ -309,6 +352,7 @@ def report_opengl_devices(request, selected): 'all_exts': all_exts, 'all_devices': all_devices, 'devices': distinct_devices, + 'gl_renderers': gl_renderers, }) def report_opengl_device(request, device): @@ -316,3 +360,55 @@ def report_opengl_device(request, device): def report_opengl_device_compare(request): return report_opengl_devices(request, request.GET.getlist('d')) + + + +def report_usercount(request): + reports = UserReport.objects.raw(''' + SELECT id, upload_date, user_id_hash + FROM userreport_userreport + ORDER BY upload_date + ''') + + users_by_date = {} + + for report in reports: + t = report.upload_date.date() # group by day + users_by_date.setdefault(t, set()).add(report.user_id_hash) + + seen_users = set() + data_scatter = ([], [], []) + for date,users in sorted(users_by_date.items()): + data_scatter[0].append(date) + data_scatter[1].append(len(users)) + data_scatter[2].append(len(users - seen_users)) + seen_users |= users + + from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas + from matplotlib.figure import Figure + from matplotlib.dates import DateFormatter + import matplotlib.artist + + fig = Figure(figsize=(12,6)) + + ax = fig.add_subplot(111) + fig.subplots_adjust(left = 0.08, right = 0.95, top = 0.95, bottom = 0.2) + + ax.plot(data_scatter[0], data_scatter[1], marker='o') + ax.plot(data_scatter[0], data_scatter[2], marker='o') + + ax.legend(('Total users', 'New users'), 'upper left', frameon=False) + matplotlib.artist.setp(ax.get_legend().get_texts(), fontsize='small') + + ax.set_ylabel('Number of users per day') + + for label in ax.get_xticklabels(): + label.set_rotation(90) + label.set_fontsize(9) + + ax.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d')) + + canvas = FigureCanvas(fig) + response = HttpResponse(content_type = 'image/png') + canvas.print_png(response, dpi=80) + return response diff --git a/source/tools/webservices/userreport/views_private.py b/source/tools/webservices/userreport/views_private.py index 6be4626312..3d0b3e1d8b 100644 --- a/source/tools/webservices/userreport/views_private.py +++ b/source/tools/webservices/userreport/views_private.py @@ -1,14 +1,19 @@ -from userreport.models import UserReport +from userreport.models import UserReport, UserReport_hwdetect from django.http import HttpResponse, HttpResponseForbidden from django.shortcuts import render_to_response from django.template import RequestContext from django.core.paginator import Paginator, InvalidPage, EmptyPage from django.db.models import Q +from django.utils import simplejson import re import numpy +class hashabledict(dict): + def __hash__(self): + return hash(tuple(sorted(self.items()))) + def render_reports(request, reports, template, args): paginator = Paginator(reports, args.get('pagesize', 100)) try: @@ -58,11 +63,12 @@ def report_performance(request): reports = UserReport.objects.order_by('upload_date') reports = reports.filter( Q(data_type = 'hwdetect', data_version__gte = 5) | - Q(data_type = 'profile', data_version__gte = 1) + Q(data_type = 'profile', data_version__gte = 2) ) + #reports = reports[:500] def summarise_hwdetect(report): - json = report.data_json() + json = report.data_json_nocache() return { 'cpu_identifier': json['cpu_identifier'], 'device': report.gl_device_identifier(), @@ -73,25 +79,46 @@ def report_performance(request): } def summarise_profile(report): - json = report.data_json() + json = report.data_json_nocache() + if 'map' in json: + mapname = json['map'] + else: + mapname = 'unknown' # e.g. random maps msecs = None + shadows = False + fancywater = False for name,table in json['profiler'].items(): m = re.match(r'Profiling Information for: root \(Time in node: (\d+\.\d+) msec/frame\)', name) if m: - #msecs = m.group(1) try: - if report.data_version == 1: - msecs = float(table['render']['msec/frame']) - else: - msecs = float(table['data']['render'][2]) + msecs = float(table['data']['render'][2]) except KeyError: pass - # TODO: get the render time? - # TODO: get shadow, water settings + + try: + if float(table['data']['render'][0]['render submissions'][0]['render shadow map'][2]): + shadows = True + except (KeyError, TypeError): + pass + + try: + if float(table['data']['render'][0]['render submissions'][0]['render reflections'][2]): + fancywater = True + except (KeyError, TypeError): + pass + + if msecs is None: + return None + + options = [] + if shadows: options.append('S') + if fancywater: options.append('W') + return { 'msecs': msecs, - 'map': json['map'], + 'map': mapname, 'time': json['time'], + 'options': '[%s]' % '+'.join(options), # 'json': json } @@ -102,17 +129,24 @@ def report_performance(request): last_hwdetect[report.user_id_hash] = summarise_hwdetect(report.downcast()) elif report.data_type == 'profile': if report.user_id_hash in last_hwdetect: - profiles.append([report.user_id_hash, last_hwdetect[report.user_id_hash], summarise_profile(report)]) + hwdetect = last_hwdetect[report.user_id_hash] + if hwdetect['build_debug']: + continue + prof = summarise_profile(report) + if prof is not None: + profiles.append([report.user_id_hash, hwdetect, prof]) datapoints = {} for user,hwdetect,profile in profiles: - if hwdetect['build_debug']: + if profile['map'] != 'Death Canyon': + continue + if profile['time'] != 5: continue -# if profile['time'] != 60: -# continue if profile['msecs'] is None or float(profile['msecs']) == 0: continue - datapoints.setdefault(hwdetect['device'], []).append(profile['msecs']) + fps = 1000.0 / profile['msecs'] + title = '%s %s' % (hwdetect['device'], profile['options']) + datapoints.setdefault(title, []).append(fps) # return render_to_response('reports/performance.html', {'data': datapoints, 'reports': profiles}) @@ -120,6 +154,8 @@ def report_performance(request): -numpy.median(v) ) + print "# %d datapoints" % sum(len(v) for k,v in sorted_datapoints) + data_boxplot = [ v for k,v in sorted_datapoints ] data_scatter = ([], []) for i in range(len(data_boxplot)): @@ -130,21 +166,110 @@ def report_performance(request): from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas from matplotlib.figure import Figure - fig = Figure(figsize=(16,16)) + fig = Figure(figsize=(16, 0.25*len(datapoints.items()))) ax = fig.add_subplot(111) - fig.subplots_adjust(left = 0.05, right = 0.99, top = 0.99, bottom = 0.2) + fig.subplots_adjust(left = 0.22, right = 0.98, top = 0.98, bottom = 0.05) - ax.boxplot(data_boxplot, sym = '') - ax.scatter(data_scatter[0], data_scatter[1], marker='x') + ax.grid(True) - ax.set_ylim(1, 10000) - ax.set_yscale('log') - ax.set_ylabel('Render time (msecs)') + ax.boxplot(data_boxplot, vert = 0, sym = '') + ax.scatter(data_scatter[1], data_scatter[0], marker='x') - ax.set_xticklabels([k for k,v in sorted_datapoints], rotation=90, fontsize=8) + ax.set_xlim(0.1, 1000) + ax.set_xscale('log') + ax.set_xlabel('Framerate (fps)') + + ax.set_yticklabels([k for k,v in sorted_datapoints], fontsize=8) + ax.set_ylabel('Device [options: Shadows + Water reflections]') canvas = FigureCanvas(fig) response = HttpResponse(content_type = 'image/png') canvas.print_png(response, dpi=80) return response + +def report_ram(request): + reports = UserReport.objects + reports = reports.filter(data_type = 'hwdetect', data_version__gte = 1) + + counts = {} + for report in reports: + #if not report.data_json()['os_linux']: continue + ram = report.data_json()['ram_total'] + counts.setdefault(ram, set()).add(report.user_id_hash) + + datapoints = [] + accum = 0 + for size,count in sorted(counts.items()): + accum += len(count) + datapoints.append((size, accum)) + + print "\n".join(repr(r) for r in datapoints) + + from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas + from matplotlib.figure import Figure + + fig = Figure(figsize=(16, 10)) + + ax = fig.add_subplot(111) + fig.subplots_adjust(left = 0.05, right = 0.98, top = 0.98, bottom = 0.05) + + ax.grid(True) + + ax.plot([ d[0] for d in datapoints ], [ 100*(1-float(d[1])/accum) for d in datapoints ]) + + ax.set_xticks([0, 256, 512] + [1024*n for n in range(1, 9)]) + ax.set_xlim(0, 8192) + ax.set_xlabel('RAM (megabytes)') + + ax.set_yticks(range(0, 101, 5)) + ax.set_ylim(0, 100) + ax.set_ylabel('Cumulative percentage of users') + + canvas = FigureCanvas(fig) + response = HttpResponse(content_type = 'image/png') + canvas.print_png(response, dpi=80) + return response + +def report_os(request): + reports = UserReport_hwdetect.objects + reports = reports.filter(data_type = 'hwdetect', data_version__gte = 1) + + counts = {} + for report in reports: + json = report.data_json() + if 'linux_release' in json: + counts.setdefault(repr(json['linux_release']), set()).add(report.user_id_hash) + os = report.os() + counts.setdefault(os, set()).add(report.user_id_hash) + + out = '' + for c in sorted(counts.items(), key = lambda c: len(c[1])): + out += '%d %s\n' % (len(c[1]), c[0]) + + return HttpResponse(out, content_type = 'text/plain') + +def report_hwdetect_test_data(request): + reports = UserReport_hwdetect.objects + reports = reports.filter(data_type = 'hwdetect', data_version__gte = 1) + + data = set() + for report in reports: + json = report.data_json_nocache() + relevant = { + 'os_unix': json['os_unix'], + 'os_linux': json['os_linux'], + 'os_macosx': json['os_macosx'], + 'os_win': json['os_win'], + 'gfx_card': json['gfx_card'], + 'gfx_drv_ver': json['gfx_drv_ver'], + 'gfx_mem': json['gfx_mem'], + 'GL_VENDOR': json['GL_VENDOR'], + 'GL_RENDERER': json['GL_RENDERER'], + 'GL_VERSION': json['GL_VERSION'], + 'GL_EXTENSIONS': json['GL_EXTENSIONS'], + } + data.add(hashabledict(relevant)) + + json = simplejson.dumps(list(data), indent=1, sort_keys=True) + return HttpResponse('var hwdetectTestData = %s' % json, content_type = 'text/plain')