Git Product home page Git Product logo

rubberband's Introduction

Rubberband

Build Status

A flexible web view and analysis platform for solver log files of mathematical optimization software, backed by Elasticsearch.

Development

This is a detailed description of how to set up Rubberband.

Installing Elasticsearch

Java 8 is required to run Elasticsearch. For Ubuntu, you can install Java 8 this way.

sudo apt-get install python-software-properties
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer

To confirm that Java is properly installed, check the version.

$ java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

Now you're ready to install Elasticsearch. NOTE: Elasticsearch is rapidly developing software. Only 2.x versions of Elasticsearch are supported by Rubberband. Sadly, Elasticsearch is neither backwards- nor forwards-compatible. Here are the most current instructions for installing Elasticsearch with apt on Ubuntu.

wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
echo "deb http://packages.elastic.co/elasticsearch/2.x/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-2.x.list
sudo apt-get update && sudo apt-get install elasticsearch

General instructions for installing Elasticsearch can be found in the offical Elasticsearch documentation.

More information about running Elasticsearch as a service can be found here, though this shouldn't be required for a development setup.

Setting up Rubberband

Rubberband is built on tornado and IPET, an interactive performance evaluation tool that comes with a parsing library for benchmark files. To get Rubberband running locally, make sure you first have Elasticsearch installed and running.

sudo service elasticsearch start

Now clone this repository and set up a virtual environment.

virtualenv -p python3 --no-site-packages venv
source venv/bin/activate
pip install -r requirements.txt
pip install -r requirements-dev.txt

The first install command will clone and install IPET from github, you don't need to do this manually.

For a deployed version of Rubberband copy the configuration file in config/app.cfg into /etc/rubberband/, and edit the required variables. Rubberband has some sane defaults already configured, so this step isn't strictly required. However, if you want to connect Rubberband to a Gitlab instance, or to an SMTP server to send email, you will need to edit app.cfg. NOTE: If you install Rubberband as a developer version, you don't need to do this.

Populating Elasticsearch

To populate the database or run unit tests, first install Rubberband inside the virtualenv.

pip install -e .

Now take a look at the control script in bin/rubberband-ctl. Running the control script with no options will show the help. For first-time, create the index, and populate that index with data. This can be accomplished with the following two commands.

bin/rubberband-ctl create-index
bin/rubberband-ctl populate-index

The second command will need a few minutes to finish. If the commands complete sucessfully, stdout should look something like this:

WARNING:elasticsearch:HEAD /solver-results [status:404 request:0.005s]
INFO:elasticsearch:PUT http://127.0.01:9200/solver-results [status:200 request:0.107s]
INFO:elasticsearch:HEAD http://127.0.01:9200/solver-results [status:200 request:0.002s]
INFO:elasticsearch:PUT http://127.0.01:9200/solver-results/_mapping/file [status:200 request:0.041s]
INFO:elasticsearch:HEAD http://127.0.01:9200/solver-results [status:200 request:0.002s]
INFO:elasticsearch:PUT http://127.0.01:9200/solver-results/_mapping/testset [status:200 request:0.019s]
INFO:root:Loading additional configuration from /etc/rubberband/app.cfg
INFO:root:Setting up Elasticsearch connection.
INFO:rubberband.utils.importer:debug opened a connection to elasticsearch with the ResultClient
INFO:rubberband.utils.importer:Found 4 files. Beginning to parse.
INFO:urllib3.connectionpool:Starting new HTTP connection (1): 127.0.01
INFO:elasticsearch:GET http://127.0.01:9200/solver-results/testset/_search [status:200 request:0.057s]
INFO:rubberband.utils.importer:Adding SoluFile.
...

Start the server

Cross your fingers and run the following command from your virtual environment.

python server.py

If everything went well, you should be able to open http://127.0.0.1:8888/ in your browser and see something that looks like this.

rubberband screenshot

Documentation

To build the documentation run the following commands from inside the virtualenvironment:

cd doc
make doc

Now you can view the documentation by opening doc/build/html/index.html in your favorite webbrowser.

Testing

Run the test suite.

py.test -v tests/

Tests will fail if Elasticsearch is not running, or if the index is empty or if you didn't configure authentication correctly.

Deployment

Rubberband currently requires a connection to an Elasticsearch 2.x instance and (optionally) a Gitlab instance to run. To configure a Gitlab connection, edit the configuration variables in /etc/rubberband/app.cfg beginning with gitlab_. The Gitlab connection is used to look up information from the code base that your test set log is linked to. Examples of this type of information are git commit date and last committer. The visualize tab is disabled if no Gitlab connection information is provided.

Authentication

There is no authentication built into Rubberband, though rubberband will authorize requests to the frontend using the X-Forwarded-Email header. Request made to the API require this header, as well as an X-Api-Token header. This is to say that authentication and setting the appropriate headers is the responsibility of the user/deployer. oauth2_proxy is a convenient proxy that, when properly hooked up to an OAuth provider, will set this header for you.

Web Server

Rubberband is meant to be deployed behind a production webserver, such as a nginx or apache. See config/rubberband-oauth for a sample nginx configuration. This example shows an HTTPS deployment configured with Let's Encrypt.

Process Management

Supervisor is a process monitor and manager, and great way to make sure Rubberband keeps running reliably in production.

Contributing

See CONTRIBUTING.md.

rubberband's People

Contributors

fschloesser avatar xmunoz avatar svigerske avatar gregorch avatar ambros-gleixner avatar ju-manns avatar dependabot[bot] avatar

Stargazers

sul217 avatar Matthias Miltenberger avatar Niklas Rieken avatar

Watchers

Ted Ralphs avatar  avatar  avatar  avatar  avatar

rubberband's Issues

initialization of ES index lost

"small changes" in b06fc5f disabled the commands to initialize and purge the ElasticSearch index, so the steps

bin/rubberband-ctl create-index
bin/rubberband-ctl populate-index

from the README.md do not work anymore.

What was the reason to remove these (@fschloesser)? And should they be added again?

Feature Request: "Shopping Cart"

The search view is restricted to 100 entries. It has now happened to me that I wanted to compare two testruns with different uploader, settings name, test set, and upload date, A and B. I can search and find A easily and select it. However, it is impossible now to search for B without losing the selection A.

I can imagine 3 different ways to cope with this, the easiest of which I would call a "shopping cart".

  1. Shopping cart: Like in online shopping, create a feature that saves selected runs permanently during repeated queries. I can select A, add it to my shopping cart, and continue searching for B, select B as well, and finally compare the two.

  2. Allow filter disjunction. Extend the filter functionality of the search view. Below every filter, add a plus button that can be clicked to select a second, third, and so on option. I could select "A" as setting, then click the + button to also select another setting "B".

  3. Allow tag assignment independently from uploading. I could tag A and B with the same tag, and then search for the tag. This option is, of course, less cool and functional than the two others.

document instance database and make it customizable

constants.py defines a location for a solu database:

ALL_SOLU = FILES_DIR + "instancedata/database/instancedb.sqlite3"

Evaluation will fail if this is not available. The README.md does not mention that this needs to be provided and what the database schema is.

In addition, the location should be customizable via the configuration file (app.cfg).
Ideally, a remote location, or a webrequest, can be specified that Rubberband can query to get the latest version of the database.

Feature request: Performance diagrams

A suggestion for a feature: Taking several variants (settings) run on the same testset, generate a performance diagram.

This could be the Dolan and Moré performance diagram, but they depend heavily on the best variant. So I would suggest to generate a "number/percentage of instances solved" (y-axis) vs. "time" (x-axis) diagram, i.e., the y-axis displays for every point in time the number/percentage of instances solved up to this time.

The figures could be generated using own code or gnuplot or tikz/pgfplots. The latter option would allow to include the figures into papers in a nice way, but one would need to also store the results in the file.

SoPlex hash

Right now, you have to look into the log file to find out the git hash of the SoPlex version used for a run. It would be nice if this would be somewhere at the top of the meta data.

Corresponding ipet issue: GregorCH/ipet#57

Filter search by queue

Now that we have many queues that we are using, it would be nice if we could filter the search results by the queue.

Solver and version column in search table

Currently the search table misses columns showing the solver name and the solver version and the LP solver version. Maybe it makes sense to display this all in one column e.g. "SCIP 4.0.0.2 (SoPlex 3.0.0.1)", but it must work generically, also for other solvers in the future that have empty LP solver column.

Legend for ipet evaluations

Marc suggested to establish a legend in rubberband that explains the columns of the ipet table, especially, but not exclusively

  • dual fail, primal fail, abort
  • limit
  • unknown
  • Q, p
  • how do counts work in the initial columns, especially when aggregating
  • a description on which evaluation was picked

I am not sure how to best implement this in a generic way. Maybe entries of the ipet xml files can obtain a description tag that is then rendered and displayed?

Probably best to brainstorm offline.

@GregorCH

Sort by date

When sorting the results of a search by one of the columns that represent a date, e.g., "Date added", the sorting is incorrect. It sorts first by day, then month (alphabetically), then year, but this should be the other way around.

Feature Request. New Console Log View of an IPET evaluation

We always improve IPET's output and warning messages. It would be nice to provide a view of IPET's evaluation warning and error output in a dedicated window in the Summary tab. Then, we can more easily spot problems if one wants to be informed if there were any problems such as a nonexisting entry of an instance in the data base, or these annoying warning messages from numpy.

Some formatting issues in the Details table

  • all numeric columns display a left-aligned string "--" for missing values which looks weird.
  • An infinite gap is not displayed consistently in the Gap column if the infinity is different from 1e+20, probably a hard-coded value somewhere
  • sometimes tiny differences in the primal bounds/dual bounds do not have a visible coloring. Maybe use some minimum redness/greenness to visibly distinguish instances where nothing changed from those where it did.

Preserve upload time and uploader at reimport

When reimporting a testrun, currently upload time and uploader change to now and the person who performs the reimport. This should not happen: it can make testruns hard to find if you don't know about the reimport.

Improve newest table

Remove filename, add meta columns: test set, settings, debug/opt, number of seeds.

Display ipet evaluation for single run statistics

The statistics page should have a bulleted list of different pre-defined ipet evaluations (xml files).

In a second step, we should be able to add a custom ipet evaluation (or a subset of customizations) with a unique URL that can be shared. For this, open a new issue once this one is closed.

Include Hostname into search table

This is the quickest proxy to see whether to runs where run on the same hardware.

PS: I like that the search table columns are now automatically displayed in the details of a testrun.

upgrade to current ElasticSearch

I spend some days to get an ElasticSearch 8.15 up and running and to start migrating data from an ElasticSearch 2.x server.
But now I noticed that the README here says that Rubberband only works with ElasticSearch 2.x. (yes, I should have seen that earlier).

I see that there is a branch upgrade-elasticsearch that hasn't been received updates for 6 years. @fschloesser Do you remember what the state of this is? What is the difficulty in using a more recent ElasticSearch?

missing eval files

constants.py defines a number of iPET evaluation files, to be expected in files/static:

IPET_EVALUATIONS = {
0: {"path": STATIC_FILES_DIR + "eval_singleruns_exclude.xml",
"name": "single runs - exclude fails & aborts (standard)"},
1: {"path": STATIC_FILES_DIR + "eval_singleruns_include.xml",
"name": "single runs - include fails & aborts (standard)"},
2: {"path": STATIC_FILES_DIR + "eval_singleruns_punish.xml",
"name": "single runs - punish fails & aborts (standard)"},
3: {"path": STATIC_FILES_DIR + "eval_groupgithash_exclude.xml",
"name": "group by githash - exclude fails & aborts"},
4: {"path": STATIC_FILES_DIR + "eval_groupsettings_exclude.xml",
"name": "group by settings - exclude fails & aborts"},
5: {"path": STATIC_FILES_DIR + "eval_grouplpsolver_exclude.xml",
"name": "group by LP solver - exclude fails & aborts"},
6: {"path": STATIC_FILES_DIR + "eval_groupsettings_exclude_detailed.xml",
"name": "detailed view - group settings, exclude"},
7: {"path": STATIC_FILES_DIR + "eval_groupsettings_include_detailed.xml",
"name": "detailed view - group settings, include"},
8: {"path": STATIC_FILES_DIR + "eval_groupgithash_exclude_detailed.xml",
"name": "detailed view - group githash, exclude"},
9: {"path": STATIC_FILES_DIR + "evaluation-pure.xml",
"name": "pure evaluation for sap"},
10: {"path": STATIC_FILES_DIR + "evaluation-deco.xml",
"name": "deco evaluation for sap"},
11: {"path": STATIC_FILES_DIR + "evalclusterbench_queue.xml",
"name": "clusterbenchmark queues"},
12: {"path": STATIC_FILES_DIR + "evalclusterbench_queuenode.xml",
"name": "clusterbenchmark queuenodes"},
13: {"path": STATIC_FILES_DIR + "papilo_evaluation.xml",
"name": "evaluation papilo"},
14: {"path": STATIC_FILES_DIR + "papilo_evaluation_solver_specific.xml",
"name": "evaluation papilo solver specific"},
15: {"path": STATIC_FILES_DIR + "papilo_evaluation_groupsettings.xml",
"name": "evaluation papilo group settings"},
}

Some of these are in staticfiles/static in this repository, but all of them are only found in directory static of ZIB-internal rubberband-config repo.

eval files should not be duplicated and the public repo should not list private eval files.
Preferably, only a directory with eval files is specified in constants.py (or app.cfg), and Rubberband just uses all it finds there (instead of having to give an explicit list).

Feature Request: Extend download functionality

It is currently a tedious process to download the out,err,meta,set-files that a colleague uploaded to rubberband. I suggest to extend the download functionality by a "tbz2" function (next to plaintext, gzip).
If selected, rubberband should

  1. group all files that belong to the current comparison, i.e., all out/err/meta/set files for all involved test runs, from elasticsearch.
  2. create a single, temporary tbz2 (bzip beats gzip) archive for download
  3. delete the archive after the download has been completed.

Add sort functionality back to Ipet table

This would be super useful, only we need to ensure that the groups can be sorted as in the XML schema.

A quick workaround would be to simply number them explicitly 1. all, 2. clean, ... in the XMLs, but maybe there is a better way.

Anyway, let's look at it together on your local development version before it goes online.

better handling of gitlab authentification error

If one has specified a GitLab token, but then there is an authentication error (e.g., because the token expired), then this leads to some uncaught exception:

01-08-2024 09:41:46 - 741 ERROR tornado.application Uncaught exception GET / (127.0.0.1)
HTTPServerRequest(protocol='http', host='rubberband.zib.de:443', method='GET', uri='/', version='HTTP/1.1', remote_ip='127.0.0.1')
Traceback (most recent call last):
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/tornado/web.py", line 1569, in _execute
    result = self.prepare()
  File "/opt/rubberband/package-20170825173100/rubberband/handlers/fe/base.py", line 27, in prepare
    self.current_user = self.get_current_user()
  File "/opt/rubberband/package-20170825173100/rubberband/handlers/fe/base.py", line 84, in get_current_user
    self.access_level = get_user_access_level(email)
  File "/opt/rubberband/package-20170825173100/rubberband/utils/gitlab.py", line 48, in get_user_access_level
    group_users = client.groups.get(group_id).members.list(query=user_mail)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/exceptions.py", line 251, in wrapped_f
    return f(*args, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/mixins.py", line 49, in get
    server_data = self.gitlab.http_get(path, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/__init__.py", line 532, in http_get
    streamed=streamed, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/__init__.py", line 506, in http_request
    response_body=result.content)
gitlab.exceptions.GitlabAuthenticationError: 401: invalid_token
01-08-2024 09:41:46 - 815 ERROR tornado.application Uncaught exception in write_error
Traceback (most recent call last):
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/tornado/web.py", line 1569, in _execute
    result = self.prepare()
  File "/opt/rubberband/package-20170825173100/rubberband/handlers/fe/base.py", line 27, in prepare
    self.current_user = self.get_current_user()
  File "/opt/rubberband/package-20170825173100/rubberband/handlers/fe/base.py", line 84, in get_current_user
    self.access_level = get_user_access_level(email)
  File "/opt/rubberband/package-20170825173100/rubberband/utils/gitlab.py", line 48, in get_user_access_level
    group_users = client.groups.get(group_id).members.list(query=user_mail)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/exceptions.py", line 251, in wrapped_f
    return f(*args, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/mixins.py", line 49, in get
    server_data = self.gitlab.http_get(path, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/__init__.py", line 532, in http_get
    streamed=streamed, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/__init__.py", line 506, in http_request
    response_body=result.content)
gitlab.exceptions.GitlabAuthenticationError: 401: invalid_token

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/tornado/web.py", line 1112, in send_error
    self.write_error(status_code, **kwargs)
  File "/opt/rubberband/package-20170825173100/rubberband/handlers/fe/base.py", line 162, in write_error
    self.render("error.html", status_code=status_code, page_title=reason, msg=log_message)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/tornado/web.py", line 766, in render
    html = self.render_string(template_name, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/tornado/web.py", line 905, in render_string
    namespace = self.get_template_namespace()
  File "/opt/rubberband/package-20170825173100/rubberband/handlers/fe/base.py", line 175, in get_template_namespace
    namespace = super(BaseHandler, self).get_template_namespace()
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/tornado/web.py", line 921, in get_template_namespace
    current_user=self.current_user,
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/tornado/web.py", line 1233, in current_user
    self._current_user = self.get_current_user()
  File "/opt/rubberband/package-20170825173100/rubberband/handlers/fe/base.py", line 84, in get_current_user
    self.access_level = get_user_access_level(email)
  File "/opt/rubberband/package-20170825173100/rubberband/utils/gitlab.py", line 48, in get_user_access_level
    group_users = client.groups.get(group_id).members.list(query=user_mail)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/exceptions.py", line 251, in wrapped_f
    return f(*args, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/mixins.py", line 49, in get
    server_data = self.gitlab.http_get(path, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/__init__.py", line 532, in http_get
    streamed=streamed, **kwargs)
  File "/opt/rubberband/production/venv/lib/python3.6/site-packages/gitlab/__init__.py", line 506, in http_request
    response_body=result.content)
gitlab.exceptions.GitlabAuthenticationError: 401: invalid_token
01-08-2024 09:41:46 - 815 ERROR tornado.access  500 GET / (127.0.0.1) 121.41ms

It would be really nice if the exception were caught and Rubberband would display some useful warning or error message instead of a 500 - internal server error.

Design improvements

These are not high priority, but I wanted to create an issue to collect design changes that would enhance the user experience. General:

  • A monospaced font: although this sounds very oldschool, I am convinced it improves readability of tables; there are some well-designed monospace fonts out there, e.g. https://fonts.google.com/specimen/Fira+Mono, which could even be good for all text.
  • Integrate the menu on the left (Search, ...) into the top bar in order to maximize usage of screen width.
  • Remove horizontally separating lines from tables (not 100% sure, let's try).
  • Rename the header "Result" to "Evaluate".
  • Update the bug report link in FAQ.
  • Update or remove the "500" explanation under FAQ.
  • Rename FAQ to Help.

In the details table

  • Replace the harsh red and green by something "nicer".
  • Move the search field to the left.
  • Move the "How do I interpret the comparison view?" text as an explanation below the detailed table.

In the Ipet table:

  • Right align numbers.
  • Remove vertically separating lines within one group.
  • Add a background color to columns abort, ..., unkn and the Q columns
  • Print instance numbers (in the first columns) as integers, without .0.
  • Format time, node, Q values, and p values with less precision; 3 digits should suffice, for time and nodes even 1.
  • Remove or shorten the "_shmean".

Enhance settings and meta tab

  • Like in the details tab, a search/filter box would be useful.
  • Can we create an option (toggle with a check box, or new tab?) such that only the parameters with different values are displayed?
  • A similar table design as the other tables would be good.

Fix README

From @tkralphs

Hey Cristina,

First of all, yay, it's running on my server at Lehigh :). I just looked over the whole thing quickly now and below are the issues I found so far. Keep in mind that we try to do these reviews as "dumb users." I was able to overcome all of these issues, but it would still be nice to improve the documentation if it can be done without much effort.

  1. For the section on installing elastic search, the instructions are specific to Ubuntu and probably won't work for most Debian or Fedora/CentOS installations. You don't necessarily need to have explicit instructions for those distros, but it would be good to indicate that what is needed is just to get java 8 installed and that instructions for that can be found by Googling. I was able to get instructions for installing java 8 on Debian pretty easily and that more or less enough to make it through getting elastic search installed.

  2. There seems to be a dependency on "click". I had to pip install "click" to get rubberband to run.

  3. You can't run "rubberband-ctl" before installing rubberband :), so the

source venv/bin/activate
pip install -e .

has to come before populating the database.

  1. The ipet dependency will fail for most users, since it's not on github. Is that necessary? I guess not, since everything seems to be OK without it. Some explanation of what role ipet plays in all of this and what can't be done without it would be good. Of course, ipet will hopfully be available publicly before long.

  2. For seeding the database, it might be nice to include some expected output or some indication of what should happen.

  3. Same for tests.

After getting past the tests, I really didn't have much idea what to do next, so I just ran

python server.py

and then was actually able to see something at

coral.ie.lehigh.edu:8888

Yay! You won't be able to do the same because the port is only open through our VPN (and I'm going to kill the server shortly). It seems as though there should be a bit more on actually starting the server and maybe some screenshots of what one should see.

More later when I start trying to upload things, etc.

Ted

update dependencies, including iPET

requirements.txt and requirements-dev.txt defines explicit versions of Python package dependencies. For iPET, some version from end of 2019 is specified.
With these dependencies, I can get Rubberband to work with Python 3.6 (EOL many years ago), but not a current 3.12.
There are also several PR from dependabot that suggest updating dependencies in order to fix security issues.

Thus, one should see that dependencies get updated. Note, that IPET is very strict on the versions of its dependencies (https://github.com/GregorCH/ipet/blob/master/requirements.txt), with most of them also being a dependency of Rubberband.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.