From f0ea32cb8dff86ee674bde133f565321f0d8d807 Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Fri, 25 Feb 2011 19:46:01 +0000 Subject: [PATCH] Import user-report server side code This was SVN commit r8986. --- source/tools/webservices/__init__.py | 0 source/tools/webservices/manage.py | 11 + source/tools/webservices/settings.py | 120 +++++++ .../webservices/settings_local.EXAMPLE.py | 20 ++ source/tools/webservices/setup.txt | 28 ++ source/tools/webservices/urls.py | 11 + .../tools/webservices/userreport/__init__.py | 0 source/tools/webservices/userreport/admin.py | 17 + source/tools/webservices/userreport/models.py | 69 ++++ .../webservices/userreport/templates/404.html | 4 + .../userreport/templates/index.html | 15 + .../userreport/templates/reports/base.html | 61 ++++ .../userreport/templates/reports/cpu.html | 52 +++ .../templates/reports/hwdetect.html | 17 + .../userreport/templates/reports/message.html | 16 + .../templates/reports/opengl_device.html | 94 ++++++ .../templates/reports/opengl_feature.html | 121 +++++++ .../templates/reports/opengl_index.html | 102 ++++++ .../userreport/templates/reports/profile.html | 41 +++ .../userreport/templates/reports/user.html | 31 ++ .../userreport/templatetags/__init__.py | 0 .../userreport/templatetags/cycle.py | 99 ++++++ .../userreport/templatetags/report_tags.py | 128 +++++++ source/tools/webservices/userreport/urls.py | 13 + .../webservices/userreport/urls_private.py | 8 + source/tools/webservices/userreport/views.py | 314 ++++++++++++++++++ .../webservices/userreport/views_private.py | 45 +++ source/tools/webservices/userreport/x86.py | 156 +++++++++ 28 files changed, 1593 insertions(+) create mode 100644 source/tools/webservices/__init__.py create mode 100644 source/tools/webservices/manage.py create mode 100644 source/tools/webservices/settings.py create mode 100644 source/tools/webservices/settings_local.EXAMPLE.py create mode 100644 source/tools/webservices/setup.txt create mode 100644 source/tools/webservices/urls.py create mode 100644 source/tools/webservices/userreport/__init__.py create mode 100644 source/tools/webservices/userreport/admin.py create mode 100644 source/tools/webservices/userreport/models.py create mode 100644 source/tools/webservices/userreport/templates/404.html create mode 100644 source/tools/webservices/userreport/templates/index.html create mode 100644 source/tools/webservices/userreport/templates/reports/base.html create mode 100644 source/tools/webservices/userreport/templates/reports/cpu.html create mode 100644 source/tools/webservices/userreport/templates/reports/hwdetect.html create mode 100644 source/tools/webservices/userreport/templates/reports/message.html create mode 100644 source/tools/webservices/userreport/templates/reports/opengl_device.html create mode 100644 source/tools/webservices/userreport/templates/reports/opengl_feature.html create mode 100644 source/tools/webservices/userreport/templates/reports/opengl_index.html create mode 100644 source/tools/webservices/userreport/templates/reports/profile.html create mode 100644 source/tools/webservices/userreport/templates/reports/user.html create mode 100644 source/tools/webservices/userreport/templatetags/__init__.py create mode 100644 source/tools/webservices/userreport/templatetags/cycle.py create mode 100644 source/tools/webservices/userreport/templatetags/report_tags.py create mode 100644 source/tools/webservices/userreport/urls.py create mode 100644 source/tools/webservices/userreport/urls_private.py create mode 100644 source/tools/webservices/userreport/views.py create mode 100644 source/tools/webservices/userreport/views_private.py create mode 100644 source/tools/webservices/userreport/x86.py diff --git a/source/tools/webservices/__init__.py b/source/tools/webservices/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tools/webservices/manage.py b/source/tools/webservices/manage.py new file mode 100644 index 0000000000..5e78ea979e --- /dev/null +++ b/source/tools/webservices/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/source/tools/webservices/settings.py b/source/tools/webservices/settings.py new file mode 100644 index 0000000000..95d74b597f --- /dev/null +++ b/source/tools/webservices/settings.py @@ -0,0 +1,120 @@ +from settings_local import DEBUG, ADMINS, DATABASES, SECRET_KEY, WFGSITE_ROOT + +TEMPLATE_DEBUG = DEBUG + +MANAGERS = ADMINS + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'UTC' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = False + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = False + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL that handles the static files served from STATIC_ROOT. +# Example: "http://static.lawrence.com/", "http://example.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin media -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# A list of locations of additional static files +STATICFILES_DIRS = () + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + '%s/userreport/templates' % WFGSITE_ROOT, +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + + 'userreport', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# 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, +# }, +# } +#} diff --git a/source/tools/webservices/settings_local.EXAMPLE.py b/source/tools/webservices/settings_local.EXAMPLE.py new file mode 100644 index 0000000000..49432fc340 --- /dev/null +++ b/source/tools/webservices/settings_local.EXAMPLE.py @@ -0,0 +1,20 @@ +# Fill in this file and save as settings_local.py + +DEBUG = True + +ADMINS = ( + ('Your Name', 'you@example.com'), +) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'wfgsite', + 'USER': 'wfgsite', + 'PASSWORD': '################', + } +} + +SECRET_KEY = '##################################################' + +WFGSITE_ROOT = '/var/####/wfgsite' diff --git a/source/tools/webservices/setup.txt b/source/tools/webservices/setup.txt new file mode 100644 index 0000000000..e2c1f3f150 --- /dev/null +++ b/source/tools/webservices/setup.txt @@ -0,0 +1,28 @@ +This has been tested with Django 1.3 Beta 1. + +To set up the database, run commands like: + + In MySQL: + create database wfgservices character set utf8; + grant create,alter,index,select,insert,update,delete on wfgservices.* to 'wfgservices'@'localhost' identified by '[PASSWORD]'; + In shell: + python manage.py syncdb + +Copy settings_local.EXAMPLE.py to settings_local.py and fill in the details. + +Set up Apache with some options kind of like: + + WSGIScriptAlias / /var/www/wherever/django.wsgi + + SetOutputFilter DEFLATE + Options None + + + AuthType Basic + AuthName "WFG login" + AuthUserFile ... + Require valid-user + + Alias /static/admin/ /usr/lib/python2.5/site-packages/django/contrib/admin/media/ + Alias /robots.txt /var/www/wherever/robots.txt + Alias /favicon.ico /var/www/wherever/favicon.ico diff --git a/source/tools/webservices/urls.py b/source/tools/webservices/urls.py new file mode 100644 index 0000000000..871f34cc2e --- /dev/null +++ b/source/tools/webservices/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import * + +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + (r'^$', 'userreport.views.index'), + (r'^report/', include('userreport.urls')), + (r'^private/', include('userreport.urls_private')), + (r'^admin/', include(admin.site.urls)), +) diff --git a/source/tools/webservices/userreport/__init__.py b/source/tools/webservices/userreport/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tools/webservices/userreport/admin.py b/source/tools/webservices/userreport/admin.py new file mode 100644 index 0000000000..ef011ec446 --- /dev/null +++ b/source/tools/webservices/userreport/admin.py @@ -0,0 +1,17 @@ +from userreport.models import UserReport +from django.contrib import admin + +class UserReportAdmin(admin.ModelAdmin): + + readonly_fields = ['uploader', 'user_id_hash', 'upload_date', 'generation_date', 'data_type', 'data_version', 'data'] + fieldsets = [ + ('User', {'fields': ['uploader', 'user_id_hash']}), + ('Dates', {'fields': ['upload_date', 'generation_date']}), + (None, {'fields': ['data_type', 'data_version', 'data']}), + ] + 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'] + date_hierarchy = 'upload_date' + +admin.site.register(UserReport, UserReportAdmin) diff --git a/source/tools/webservices/userreport/models.py b/source/tools/webservices/userreport/models.py new file mode 100644 index 0000000000..8ca1d4eb45 --- /dev/null +++ b/source/tools/webservices/userreport/models.py @@ -0,0 +1,69 @@ +from django.db import models +from django.utils import simplejson + +class UserReport(models.Model): + + uploader = models.IPAddressField(editable = False) + + # Hex SHA-1 digest of user's reported ID + # (The hashing means that publishing the database won't let people upload + # faked reports under someone else's user ID, and also ensures a simple + # consistent structure) + user_id_hash = models.CharField(max_length = 40, db_index = True, editable = False) + + # When the server received the upload + upload_date = models.DateTimeField(auto_now_add = True, db_index = True, editable = False) + + # When the user claims to have generated the report + generation_date = models.DateTimeField(editable = False) + + data_type = models.CharField(max_length = 16, db_index = True, editable = False) + + data_version = models.IntegerField(editable = False) + + data = models.TextField(editable = False) + + def data_json(self): + if not hasattr(self, 'cached_json'): + try: + self.cached_json = simplejson.loads(self.data) + except: + self.cached_json = None + return self.cached_json + +class UserReport_hwdetect(UserReport): + + class Meta: + proxy = True + + def os(self): + os = 'Unknown' + json = self.data_json() + if json: + if json['os_win']: + os = 'Windows' + elif json['os_linux']: + os = 'Linux' + elif json['os_macosx']: + os = 'OS X' + return os + + 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(' ')) + + def gl_limits(self): + json = self.data_json() + if json is None: + return None + + limits = {} + for (k, v) in json.items(): + if not k.startswith('GL_'): + continue + if k in ('GL_RENDERER', 'GL_VENDOR', 'GL_EXTENSIONS'): + continue + limits[k] = v + return limits diff --git a/source/tools/webservices/userreport/templates/404.html b/source/tools/webservices/userreport/templates/404.html new file mode 100644 index 0000000000..9b4a0efd9f --- /dev/null +++ b/source/tools/webservices/userreport/templates/404.html @@ -0,0 +1,4 @@ + + +404 +File not found. diff --git a/source/tools/webservices/userreport/templates/index.html b/source/tools/webservices/userreport/templates/index.html new file mode 100644 index 0000000000..12843ce750 --- /dev/null +++ b/source/tools/webservices/userreport/templates/index.html @@ -0,0 +1,15 @@ + + +0 A.D. report service + + +

This site collects opt-in automatic feedback from players of 0 A.D. + +

Published data: +OpenGL capabilities. +CPU features. diff --git a/source/tools/webservices/userreport/templates/reports/base.html b/source/tools/webservices/userreport/templates/reports/base.html new file mode 100644 index 0000000000..c6794d96f0 --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/base.html @@ -0,0 +1,61 @@ + + +{% block title %}Report{% endblock %} + + +

{% block heading %}Report{% endblock %}

+ +{% block content %}{% endblock %} + +{% if report_page %}{% if report_page.has_previous or report_page.has_next %} + +{% endif %}{% endif %} \ No newline at end of file diff --git a/source/tools/webservices/userreport/templates/reports/cpu.html b/source/tools/webservices/userreport/templates/reports/cpu.html new file mode 100644 index 0000000000..8a48b75dd9 --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/cpu.html @@ -0,0 +1,52 @@ +{% extends "reports/base.html" %} +{% load report_tags %} + +{% block css %} +table.profile td { + line-height: inherit; + border-bottom: inherit; + padding: 0 1em 0 0.5em; +} +.treemarker { + color: #666; + font-family: monospace; + white-space: pre; +} +{% endblock %} + +{% block title %} +CPU capabilities report +{% endblock %} + +{% block heading %} +CPU capabilities +{% endblock %} + +{% block content %} +

Based on data submitted by players of 0 A.D.

+ +

See the index page for more stuff.

+ + + + +
OS + Identifier + V/M/F + Freq + Num procs + Caches (data/instruction/unified) + TLBs + Feature bits +{% for cpu,users in cpus|sortedcpuitems %} +
{{ cpu.os }} + {{ cpu.cpu_identifier }} + {{ cpu.x86_vendor }}/{{ cpu.x86_model }}/{{ cpu.x86_family }} + {% if cpu.cpu_frequency = -1 %}?{% else %}{{ cpu.cpu_frequency|cpufreqformat }}{% endif %} + {{ cpu.cpu_numpackages }}×{{ cpu.cpu_coresperpackage }}×{{ cpu.cpu_logicalpercore }} = {{ cpu.cpu_numprocs }} + {{ cpu.caches|join:"
" }}
+
{{ cpu.tlbs|join:"
" }}
+
{% for cap in cpu.caps|sort %}{% if cap in x86_cap_descs %}{{ cap }}{% else %}{{ cap }}{% endif %} {% endfor %} +{% endfor %} +
+{% endblock %} diff --git a/source/tools/webservices/userreport/templates/reports/hwdetect.html b/source/tools/webservices/userreport/templates/reports/hwdetect.html new file mode 100644 index 0000000000..c213221b79 --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/hwdetect.html @@ -0,0 +1,17 @@ +{% extends "reports/base.html" %} +{% load report_tags %} + +{% block content %} + + + +
Received + User + Data +{% for report in report_page.object_list %} +
{{ report.upload_date|date:"Y-m-d" }} {{ report.upload_date|date:"H:i:s" }} + {{ report.user_id_hash|slice:"0:8" }} + {{ report.data|prettify_json }} +{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/source/tools/webservices/userreport/templates/reports/message.html b/source/tools/webservices/userreport/templates/reports/message.html new file mode 100644 index 0000000000..7432f38f2f --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/message.html @@ -0,0 +1,16 @@ +{% extends "reports/base.html" %} + +{% block content %} + + + +
Received + User + Message +{% for report in report_page.object_list %} +
{{ report.upload_date|date:"Y-m-d" }} {{ report.upload_date|date:"H:i:s" }} + {{ report.user_id_hash|slice:"0:8" }} + {{ report.data }} +{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/source/tools/webservices/userreport/templates/reports/opengl_device.html b/source/tools/webservices/userreport/templates/reports/opengl_device.html new file mode 100644 index 0000000000..76ea7c5033 --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/opengl_device.html @@ -0,0 +1,94 @@ +{% extends "reports/base.html" %} +{% load report_tags %} +{% load cycle %} + +{% block css %} +th.alt, td.alt { background: #f6f6f6; } +td.true { background: #3f3; } +td.false { background: #f33; } +td.true.alt { background: #2e2; } +td.false.alt { background: #e22; } + +.device-status ul { + margin: 0; + padding: 0; +} + +.device-status li { + list-style: none; +} +{% endblock %} + +{% block title %} +OpenGL capabilities report: {{ selected|join:' vs. ' }} +{% endblock %} + +{% block heading %} +OpenGL capabilities report +{% endblock %} + +{% block content %} + +

(Back to index page.)

+ +

The table here shows the features reported for the following devices:

+ +

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.

+ +

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.renderer }} ({{ device.0.os }}): +
    {% for driver in device.1 %}
  • {{ driver }}{% endfor %}
+ {% endfor %} + {% endif %} + +
{{ ext }} (spec) + {% for device in devices %} + + {% endfor %} +{% endfor %} + +{% for limit in all_limits %} + {% if not forloop.counter|mod:30 %} +
+ {% for device in devices %} + + {{ device.0.renderer }} ({{ device.0.os }}): +
    {% for driver in device.1 %}
  • {{ driver }}{% endfor %}
+ {% endfor %} + {% endif %} + +
{{ limit|prettify_gl_title }} + {% for device in devices %} + {{ device.2.0|dictget:limit }} + {% endfor %} +{% endfor %} +
+ +

Compare with other devices

+
+ + +
+ +{% endblock %} \ No newline at end of file diff --git a/source/tools/webservices/userreport/templates/reports/opengl_feature.html b/source/tools/webservices/userreport/templates/reports/opengl_feature.html new file mode 100644 index 0000000000..7e86b2e67a --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/opengl_feature.html @@ -0,0 +1,121 @@ +{% extends "reports/base.html" %} +{% load report_tags %} +{% load cycle %} + +{% block css %} +ul { + margin: 0; +} + +.support-status .r { + font-weight: bold; +} + +.support-status ul { + margin: 0; + padding: 0; +} + +.support-status li { + list-style: none; +} + +tr.head { + background: #ddd; +} + +tr.data:hover { + background: #ddd; +} +{% endblock %} + +{% block title %} +OpenGL capabilities report: {{ feature }} +{% endblock %} + +{% block heading %} +OpenGL capabilities report: {{ feature }} +{% endblock %} + +{% block content %} + +

(Back to index page.)

+ +{% if is_extension %} + +

View specification for {{ feature }}.

+ + + + + + + + + +

Supported by:

+ +
Vendor + Renderer + OS + Driver versions + +{% for device in values.true|sorteddeviceitems %} +
{{ device.0.vendor }} +{{ device.0.renderer }} +{{ device.0.os }} +
    +{% for driver in device.1|sort %}
  • {{ driver }}{% endfor %} +
+{% endfor %} + +

Not supported by:

+ +
Vendor + Renderer + OS + Driver versions + +{% for device in values.false|sorteddeviceitems %} +
{{ device.0.vendor }} +{{ device.0.renderer }} +{{ device.0.os }} +
    +{% for driver in device.1|sort %}
  • {{ driver }}{% endfor %} +
+{% endfor %} + +
+ +{% else %} + + + + {% for val in values.keys|sortreversed %} + + + + +

Value: {{ val|default_if_none:"Unsupported/unknown" }}

+ +
Vendor + Renderer + OS + Driver versions + +{% for device in values|dictget:val|sorteddeviceitems %} +
{{ device.0.vendor }} +{{ device.0.renderer }} +{{ device.0.os }} +
    +{% for driver in device.1|sort %}
  • {{ driver }}{% endfor %} +
+{% endfor %} + + {% endfor %} + +
+ +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/source/tools/webservices/userreport/templates/reports/opengl_index.html b/source/tools/webservices/userreport/templates/reports/opengl_index.html new file mode 100644 index 0000000000..3bc8db16d2 --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/opengl_index.html @@ -0,0 +1,102 @@ +{% extends "reports/base.html" %} +{% load report_tags %} + +{% block css %} + +.col { + float: left; +} + +.progress { + font-size: 10px; + font-weight: bold; + display: inline-block; + background: #f99; + width: 40px; + height: 13px; +} +.progress .bar { + display: inline-block; + position: absolute; + background: #3f3; + height: 13px; + z-index: 0; +} + +li:hover { + background: #ddd; +} + +{% endblock %} + +{% block title %} +OpenGL capabilities database - index +{% endblock %} + +{% block heading %} +OpenGL capabilities database +{% endblock %} + +{% block content %} + + + +

Based on data submitted by players of 0 A.D. +

Browse the data here, or download as JSON. +Feel free to do whatever you want with the data.

+

See the index page for more stuff.

+ +
+

Extension support

+Sort by +% support / +name. + + +

Implementation limits

+ +
+ +
+

Device details

+ +
+ +{% endblock %} \ No newline at end of file diff --git a/source/tools/webservices/userreport/templates/reports/profile.html b/source/tools/webservices/userreport/templates/reports/profile.html new file mode 100644 index 0000000000..e0a66f3ec5 --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/profile.html @@ -0,0 +1,41 @@ +{% extends "reports/base.html" %} +{% load report_tags %} + +{% block css %} +table.profile td { + line-height: inherit; + border-bottom: inherit; + padding: 0 1em 0 0.5em; +} +.treemarker { + color: #666; + font-family: monospace; + white-space: pre; +} +{% endblock %} + +{% block content %} + + + +
Received + User + Profiler +{% for report in report_page.object_list %} +{% with json=report.data_json %} +
{{ report.upload_date|date:"Y-m-d" }} {{ report.upload_date|date:"H:i:s" }} + {{ report.user_id_hash|slice:"0:8" }} + +

Time: {{ json.time }} +

Map: {{ json.map }} +{% for name,table in json.profiler.items %} +

{{ name }}

+ +{{ table|format_profile }} +
+{% endfor %} + +{% endwith %} +{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/source/tools/webservices/userreport/templates/reports/user.html b/source/tools/webservices/userreport/templates/reports/user.html new file mode 100644 index 0000000000..2e8df66f81 --- /dev/null +++ b/source/tools/webservices/userreport/templates/reports/user.html @@ -0,0 +1,31 @@ +{% extends "reports/base.html" %} +{% load report_tags %} + +{% block css %} +td.rawdata { + white-space: pre; +} +{% endblock %} + +{% block title %} +User {{ user }} +{% endblock %} + +{% block heading %} +All reports from user {{ user }} +{% endblock %} + +{% block content %} + + + +
Received + Type + Raw data +{% for report in report_page.object_list %} +
{{ report.upload_date|date:"Y-m-d" }} {{ report.upload_date|date:"H:i:s" }} + {{ report.data_type }} (v{{ report.data_version }}) + {{ report.data|prettify_json }} +{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/source/tools/webservices/userreport/templatetags/__init__.py b/source/tools/webservices/userreport/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tools/webservices/userreport/templatetags/cycle.py b/source/tools/webservices/userreport/templatetags/cycle.py new file mode 100644 index 0000000000..5f36bb9aa4 --- /dev/null +++ b/source/tools/webservices/userreport/templatetags/cycle.py @@ -0,0 +1,99 @@ +# http://code.djangoproject.com/attachment/ticket/5908/cycle.py + +from django.utils.translation import ungettext, ugettext as _ +from django.utils.encoding import force_unicode +from django import template +from django.template import defaultfilters +from django.template import Node, Variable +from django.conf import settings +from itertools import cycle as itertools_cycle + +register = template.Library() + + + +class SafeCycleNode(Node): + def __init__(self, cyclevars, variable_name=None): + self.cyclevars = cyclevars + self.cycle_iter = itertools_cycle(cyclevars) + self.variable_name = variable_name + + def render(self, context): + if context.has_key('forloop'): + if not context.get(self): + context[self] = True + self.cycle_iter = itertools_cycle(self.cyclevars) + value = self.cycle_iter.next() + value = Variable(value).resolve(context) + if self.variable_name: + context[self.variable_name] = value + return value + + + +#@register.tag +def safe_cycle(parser, token): + """ + Cycles among the given strings each time this tag is encountered. + + Within a loop, cycles among the given strings each time through + the loop:: + + {% for o in some_list %} + + ... + + {% endfor %} + + Outside of a loop, give the values a unique name the first time you call + it, then use that name each sucessive time through:: + + ... + ... + ... + + You can use any number of values, seperated by spaces. Commas can also + be used to separate values; if a comma is used, the cycle values are + interpreted as literal strings. + """ + + # Note: This returns the exact same node on each {% cycle name %} call; + # that is, the node object returned from {% cycle a b c as name %} and the + # one returned from {% cycle name %} are the exact same object. This + # shouldn't cause problems (heh), but if it does, now you know. + # + # Ugly hack warning: this stuffs the named template dict into parser so + # that names are only unique within each template (as opposed to using + # a global variable, which would make cycle names have to be unique across + # *all* templates. + + args = token.split_contents() + + if len(args) < 2: + raise TemplateSyntaxError("'cycle' tag requires at least two arguments") + + if ',' in args[1]: + # Backwards compatibility: {% cycle a,b %} or {% cycle a,b as foo %} + # case. + args[1:2] = ['"%s"' % arg for arg in args[1].split(",")] + + if len(args) == 2: + # {% cycle foo %} case. + name = args[1] + if not hasattr(parser, '_namedCycleNodes'): + raise TemplateSyntaxError("No named cycles in template." + " '%s' is not defined" % name) + if not name in parser._namedCycleNodes: + raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) + return parser._namedCycleNodes[name] + + if len(args) > 4 and args[-2] == 'as': + name = args[-1] + node = SafeCycleNode(args[1:-2], name) + if not hasattr(parser, '_namedCycleNodes'): + parser._namedCycleNodes = {} + parser._namedCycleNodes[name] = node + else: + node = SafeCycleNode(args[1:]) + return node +safe_cycle = register.tag(safe_cycle) diff --git a/source/tools/webservices/userreport/templatetags/report_tags.py b/source/tools/webservices/userreport/templatetags/report_tags.py new file mode 100644 index 0000000000..c7daf17641 --- /dev/null +++ b/source/tools/webservices/userreport/templatetags/report_tags.py @@ -0,0 +1,128 @@ +# coding=utf-8 + +from django import template +from django.template.defaultfilters import stringfilter +from django.utils import simplejson +from django.utils.safestring import mark_safe +from django.utils.html import conditional_escape + +register = template.Library() + +@register.filter +def mod(value, arg): + return value % arg + +@register.filter +@stringfilter +def has_token(value, token): + "Returns whether a space-separated list of tokens contains a given token" + return token in value.split(' ') + +@register.filter +@stringfilter +def wrap_at_underscores(value): + return value.replace('_', '​_') +wrap_at_underscores.is_safe = True + +@register.filter +@stringfilter +def prettify_json(value): + try: + data = simplejson.loads(value) + return simplejson.dumps(data, indent=2, sort_keys=True) + except: + return value + +@register.filter +@stringfilter +def glext_spec_link(value): + c = value.split('_', 2) + return 'http://www.opengl.org/registry/specs/%s/%s.txt' % (c[1], c[2]) + +@register.filter +@stringfilter +def prettify_gl_title(value): + if value.startswith('GL_FRAGMENT_PROGRAM_ARB.'): + return value[24:] + ' (fragment)' + if value.startswith('GL_VERTEX_PROGRAM_ARB.'): + return value[22:] + ' (vertex)' + return value + +@register.filter +def dictget(value, key): + return value.get(key, '') + +@register.filter +def sorteditems(value): + return sorted(value.items(), key = lambda (k, v): k) + +@register.filter +def sorteddeviceitems(value): + return sorted(value.items(), key = lambda (k, v): (k['vendor'], k['renderer'], k['os'], v)) + +@register.filter +def sortedcpuitems(value): + return sorted(value.items(), key = lambda (k, v): (k['x86_vendor'], k['x86_model'], k['x86_family'], k['cpu_identifier'])) + +@register.filter +def cpufreqformat(value): + return mark_safe("%.2f GHz" % (int(value)/1000000000.0)) + +@register.filter +def sort(value): + return sorted(value) + +@register.filter +def sortreversed(value): + return reversed(sorted(value)) + +@register.filter +def reverse(value): + return reversed(value) + +@register.filter +def format_profile(table): + cols = set() + def extract_cols(t): + for name, row in t.items(): + for n in row: + cols.add(n) + if 'children' in row: + extract_cols(row['children']) + extract_cols(table) + if 'children' in cols: + cols.remove('children') + + if 'msec/frame' in cols: + cols = ('msec/frame', 'calls/frame', '%/frame', '%/parent', 'mem allocs') + else: + cols = sorted(cols) + + + out = [''] + for c in cols: + out.append(u'%s' % conditional_escape(c)) + + def handle(indents, indent, t): + if 'msec/frame' in cols: + items = [d[1] for d in sorted((-float(r.get('msec/frame', '')), (n,r)) for (n,r) in t.items())] + else: + items = sorted(t.items()) + + item_id = 0 + for name, row in items: + if item_id == len(items) - 1: + last = True + else: + last = False + item_id += 1 + + out.append(u'') + out.append(u'%s%s─%s╴%s' % (indent, (u'└' if last else u'├'), (u'┬' if 'children' in row else u'─'), conditional_escape(name))) + for c in cols: + out.append(u'%s%s' % ('  ' * indents, conditional_escape(row.get(c, '')))) + if 'children' in row: + handle(indents+1, indent+(u' ' if last else u'│ '), row['children']) + handle(0, u'', table) + + return mark_safe(u'\n'.join(out)) diff --git a/source/tools/webservices/userreport/urls.py b/source/tools/webservices/userreport/urls.py new file mode 100644 index 0000000000..f51371ac2a --- /dev/null +++ b/source/tools/webservices/userreport/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + (r'^upload/v1/$', 'userreport.views.upload'), + + (r'^opengl/$', 'userreport.views.report_opengl_index'), + (r'^opengl/json$', 'userreport.views.report_opengl_json'), + (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'), +) diff --git a/source/tools/webservices/userreport/urls_private.py b/source/tools/webservices/userreport/urls_private.py new file mode 100644 index 0000000000..fa12526905 --- /dev/null +++ b/source/tools/webservices/userreport/urls_private.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + (r'^hwdetect/$', 'userreport.views_private.report_hwdetect'), + (r'^messages/$', 'userreport.views_private.report_messages'), + (r'^profile/$', 'userreport.views_private.report_profile'), + (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 new file mode 100644 index 0000000000..9797828b1c --- /dev/null +++ b/source/tools/webservices/userreport/views.py @@ -0,0 +1,314 @@ +from userreport.models import UserReport, UserReport_hwdetect +import userreport.x86 as x86 + +import hashlib +import datetime +import zlib + +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.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST + +class hashabledict(dict): + def __hash__(self): + return hash(tuple(sorted(self.items()))) + +@csrf_exempt +@require_POST +def upload(request): + + try: + decompressed = zlib.decompress(request.raw_post_data) + except zlib.error: + return HttpResponseBadRequest('Invalid POST data.\n', content_type = 'text/plain') + + POST = QueryDict(decompressed) + + try: + user_id = POST['user_id'] + generation_date = datetime.datetime.utcfromtimestamp(int(POST['time'])) + data_type = POST['type'] + data_version = int(POST['version']) + data = POST['data'] + except KeyError, e: + return HttpResponseBadRequest('Missing required fields.\n', content_type = 'text/plain') + + uploader = request.META['REMOTE_ADDR'] + # Fix the IP address if running via proxy on localhost + if uploader == '127.0.0.1': + try: + uploader = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0].strip() + except KeyError: + pass + + user_id_hash = hashlib.sha1(user_id).hexdigest() + + report = UserReport( + uploader = uploader, + user_id_hash = user_id_hash, + generation_date = generation_date, + data_type = data_type, + data_version = data_version, + data = data + ) + report.save() + + return HttpResponse('OK', content_type = 'text/plain') + + +def index(request): + return render_to_response('index.html') + + +def report_cpu(request): + reports = UserReport_hwdetect.objects + reports = reports.filter(data_type = 'hwdetect', data_version__gte = 4) + + all_users = set() + cpus = {} + + for report in reports: + json = report.data_json() + if json is None: + continue + + cpu = {} + for x in ( + 'x86_vendor', 'x86_model', 'x86_family', + 'cpu_identifier', 'cpu_frequency', + 'cpu_numprocs', 'cpu_numpackages', 'cpu_coresperpackage', 'cpu_logicalpercore', + 'cpu_numcaches', + 'cpu_pagesize', 'cpu_largepagesize', + 'numa_numnodes', 'numa_factor', 'numa_interleaved', + ): + cpu[x] = json[x] + + cpu['os'] = report.os() + + def fmt_size(s): + if s % (1024*1024) == 0: + return "%d MB" % (s / (1024*1024)) + if s % 1024 == 0: + return "%d kB" % (s / 1024) + return "%d B" % s + + def fmt_assoc(w): + if w == 255: + return 'fully-assoc' + else: + return '%d-way' % w + + def fmt_cache(c): + types = ('?', 'D', 'I ', 'U') + return "L%d %s: %s (%s, shared %dx, %dB line)" % ( + c['level'], types[c['type']], fmt_size(c['totalsize']), + fmt_assoc(c['associativity']), c['sharedby'], c['linesize'] + ) + + def fmt_tlb(c): + types = ('?', 'D', 'I ', 'U') + return "L%d %s: %d-entry (%s, %s page)" % ( + c['level'], types[c['type']], c['entries'], + fmt_assoc(c['associativity']), fmt_size(c['pagesize']) + ) + + def fmt_caches(d, i, cb): + dcaches = d[:] + icaches = i[:] + caches = [] + while len(dcaches) or len(icaches): + if len(dcaches) and len(icaches) and dcaches[0] == icaches[0]: + caches.append(cb(dcaches[0])) + dcaches.pop(0) + icaches.pop(0) + else: + if len(dcaches): + caches.append(cb(dcaches[0])) + dcaches.pop(0) + if len(icaches): + caches.append(cb(icaches[0])) + 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) + + caps = set() + for (n,_,b) in x86.cap_bits: + if n.endswith('[2]'): + continue + if json['x86_caps[%d]' % (b / 32)] & (1 << (b % 32)): + caps.add(n) + cpu['caps'] = frozenset(caps) + + all_users.add(report.user_id_hash) + cpus.setdefault(hashabledict(cpu), set()).add(report.user_id_hash) + + 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 = {} + + for report in reports: + json = report.data_json() + if json is None: + continue + + exts = report.gl_extensions() + limits = report.gl_limits() + + devices.setdefault(hashabledict({'vendor': json['GL_VENDOR'], 'renderer': json['GL_RENDERER'], 'os': report.os()}), {}).setdefault((hashabledict(limits), exts), set()).add(json['gfx_drv_ver']) + + 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]['vendor'], x[0]['renderer'], x[0]['os'], x)) + + data = [] + for r,vs,(limits,exts) in distinct_devices: + data.append({'device': r, 'versions': vs, 'limits': limits, 'extensions': sorted(exts)}) + json = simplejson.dumps(data, indent=1, sort_keys=True) + return HttpResponse(json, content_type = 'text/plain') + +def device_identifier(json): + return json['GL_RENDERER'] + +def report_opengl_index(request): + reports = get_hwdetect_reports() + + all_limits = set() + all_exts = set() + all_devices = set() + ext_devices = {} + + for report in reports: + json = report.data_json() + if json is None: + continue + + device = device_identifier(json) + all_devices.add(device) + + 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()) + + all_limits = sorted(all_limits) + all_exts = sorted(all_exts) + all_devices = sorted(all_devices) + + return render_to_response('reports/opengl_index.html', { + 'all_limits': all_limits, + 'all_exts': all_exts, + 'all_devices': all_devices, + 'ext_devices': ext_devices, + }) + +def report_opengl_feature(request, feature): + reports = get_hwdetect_reports() + + all_values = set() + values = {} + is_extension = False + + for report in reports: + json = report.data_json() + if json is None: + continue + + 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': json['GL_RENDERER'], 'os': report.os()}), set()).add(json['gfx_drv_ver']) + + if values.keys() == [None]: + raise Http404 + + if is_extension: + values = { + 'true': values.get(True, {}), + 'false': values.get(None, {}), + } + + return render_to_response('reports/opengl_feature.html', { + 'feature': feature, + 'all_values': all_values, + 'values': values, + 'is_extension': is_extension, + }) + +def report_opengl_devices(request, selected): + reports = get_hwdetect_reports() + + all_limits = set() + all_exts = set() + all_devices = set() + devices = {} + + for report in reports: + json = report.data_json() + if json is None: + continue + + device = device_identifier(json) + all_devices.add(device) + if device not in selected: + continue + + exts = report.gl_extensions() + all_exts |= exts + + limits = report.gl_limits() + all_limits |= set(limits.keys()) + + devices.setdefault(hashabledict({'vendor': json['GL_VENDOR'], 'renderer': json['GL_RENDERER'], 'os': report.os()}), {}).setdefault((hashabledict(limits), exts), set()).add(json['gfx_drv_ver']) + + if len(selected) == 1 and len(devices) == 0: + raise Http404 + + all_limits = sorted(all_limits) + all_exts = sorted(all_exts) + + 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]['vendor'], x[0]['renderer'], x[0]['os'], x)) + + return render_to_response('reports/opengl_device.html', { + 'selected': selected, + 'all_limits': all_limits, + 'all_exts': all_exts, + 'all_devices': all_devices, + 'devices': distinct_devices, + }) + +def report_opengl_device(request, device): + return report_opengl_devices(request, [device]) + +def report_opengl_device_compare(request): + return report_opengl_devices(request, request.GET.getlist('d')) diff --git a/source/tools/webservices/userreport/views_private.py b/source/tools/webservices/userreport/views_private.py new file mode 100644 index 0000000000..b96708c91c --- /dev/null +++ b/source/tools/webservices/userreport/views_private.py @@ -0,0 +1,45 @@ +from userreport.models import UserReport + +from django.http import HttpResponseForbidden +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.core.paginator import Paginator, InvalidPage, EmptyPage + +def render_reports(request, reports, template, args): + paginator = Paginator(reports, args.get('pagesize', 100)) + try: + page = int(request.GET.get('page', '1')) + except ValueError: + page = 1 + try: + report_page = paginator.page(page) + except (EmptyPage, InvalidPage): + report_page = paginator.page(paginator.num_pages) + + args['report_page'] = report_page + return render_to_response(template, args) + + +def report_user(request, user): + reports = UserReport.objects.order_by('-upload_date') + reports = reports.filter(user_id_hash = user) + + return render_reports(request, reports, 'reports/user.html', {'user': user}) + +def report_messages(request): + reports = UserReport.objects.order_by('-upload_date') + reports = reports.filter(data_type = 'message', data_version__gte = 1) + + return render_reports(request, reports, 'reports/message.html', {}) + +def report_profile(request): + reports = UserReport.objects.order_by('-upload_date') + reports = reports.filter(data_type = 'profile', data_version__gte = 1) + + return render_reports(request, reports, 'reports/profile.html', {'pagesize': 20}) + +def report_hwdetect(request): + reports = UserReport.objects.order_by('-upload_date') + reports = reports.filter(data_type = 'hwdetect', data_version__gte = 1) + + return render_reports(request, reports, 'reports/hwdetect.html', {}) diff --git a/source/tools/webservices/userreport/x86.py b/source/tools/webservices/userreport/x86.py new file mode 100644 index 0000000000..08839e67ea --- /dev/null +++ b/source/tools/webservices/userreport/x86.py @@ -0,0 +1,156 @@ +# CPUID feature bits, from LSB to MSB: +# (Names and descriptions gathered from various Intel and AMD sources) + +cap_raw = ( +# EAX=01H ECX: +"""SSE3 +PCLMULQDQ +DTES64: 64-bit debug store +MONITOR: MONITOR/MWAIT +DS-CPL: CPL qualified debug store +VMX: virtual machine extensions +SMX: safer mode extensions +EST: enhanced SpeedStep +TM2: thermal monitor 2 +SSSE3 +CNXT-ID: L1 context ID +?(ecx11) +FMA: fused multiply add +CMPXCHG16B +xTPR: xTPR update control +PDCM: perfmon and debug capability +?(ecx16) +PCID: process context identifiers +DCA: direct cache access +SSE4_1 +SSE4_2 +x2APIC: extended xAPIC support +MOVBE +POPCNT +TSC-DEADLINE +AES +XSAVE: XSAVE instructions supported +OSXSAVE: XSAVE instructions enabled +AVX +F16C: half-precision convert +?(ecx30) +RAZ: used by hypervisor to indicate guest status +""" + + +# EAX=01H EDX: +"""FPU +VME: virtual 8086 mode enhancements +DE: debugging extension +PSE: page size extension +TSC: time stamp counter +MSR: model specific registers +PAE: physical address extension +MCE: machine-check exception +CMPXCHG8 +APIC +?(edx10) +SEP: fast system call +MTRR: memory type range registers +PGE: page global enable +MCA: machine-check architecture +CMOV +PAT: page attribute table +PSE-36: 36-bit page size extension +PSN: processor serial number +CLFSH: CLFLUSH +?(edx20) +DS: debug store +ACPI +MMX +FXSR: FXSAVE and FXSTOR +SSE +SSE2 +SS: self-snoop +HTT: hyper-threading +TM: thermal monitor +?(edx30) +PBE: pending break enable +""" + + +# EAX=80000001H ECX: +"""LAHF: LAHF/SAHF instructions +CMP: core multi-processing legacy mode +SVM: secure virtual machine +ExtApic +AltMovCr8 +ABM: LZCNT instruction +SSE4A +MisAlignSse +3DNowPrefetch +OSVW: OS visible workaround +IBS: instruction based sampling +XOP: extended operation support +SKINIT +WDT: watchdog timer support +?(ext:ecx14) +LWP: lightweight profiling support +FMA4: 4-operand FMA +?(ext:ecx17) +?(ext:ecx18) +NodeId +?(ext:ecx20) +TBM: trailing bit manipulation extensions +TopologyExtensions +?(ext:ecx23) +?(ext:ecx24) +?(ext:ecx25) +?(ext:ecx26) +?(ext:ecx27) +?(ext:ecx28) +?(ext:ecx29) +?(ext:ecx30) +?(ext:ecx31) +""" + + +# EAX=80000001H ECX: +"""FPU[2] +VME[2] +DE[2] +PSE[2] +TSC[2] +MSR[2] +PAE[2] +MCE[2] +CMPXCHG8[2] +APIC[2] +?(ext:edx10) +SYSCALL: SYSCALL/SYSRET instructions +MTRR[2] +PGE[2] +MCA[2] +CMOV[2] +PAT[2] +PSE36[2] +?(ext:edx18) +MP: MP-capable +NX: no execute bit +?(ext:edx21) +MmxExt +MMX[2] +FXSR[2] +FFXSR +1GB: 1GB pages +RDTSCP +?(ext:edx28) +x86-64 +3DNowExt +3DNow +""" +) + +cap_bits = [] +cap_descs = {} +idx = 0 +for c in cap_raw.strip().split('\n'): + s = c.split(':') + if len(s) == 1: + cap_bits.append((s[0], None, idx)) + else: + cap_bits.append((s[0], s[1], idx)) + cap_descs[s[0]] = s[1] + idx += 1