mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
Sponsorship changes (#4274)
Rotate sponsors on the top of the page: - `topIcon` -> `topIconShowEvery` - `Sponsors` becomes an interface with methods to get levels, pick sponsors etc - A somewhat acceptable algorithm for generating a "fair choice" of sponsors given the constraints: - "show at least 1 in N times" - don't unfairly show one sponsor more than any other at the same level - Sponsor icons are loaded dynamically via a "bits" handler (introducing a short delay in them appearing), but this means the index.html doesn't change on every load, so it's still cacheable
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
---
|
||||
maxTopIcons: 3
|
||||
levels:
|
||||
- name: Corporate Sponsors
|
||||
description: 'Enormous thanks to our corporate sponsors. Please visit their websites below:'
|
||||
@@ -9,7 +10,7 @@ levels:
|
||||
img: https://static.ce-cdn.net/SOLID+SANDS-LOGO-RGB-500px.png
|
||||
url: https://solidsands.com/
|
||||
priority: 100
|
||||
topIcon: true
|
||||
topIconShowEvery: 3
|
||||
statsId: solid_sands
|
||||
- name: Intel
|
||||
description: We engineer solutions for our customers’ greatest challenges with reliable, cloud to edge computing, inspired by Moore’s Law.
|
||||
@@ -17,7 +18,7 @@ levels:
|
||||
img: https://static.ce-cdn.net/intel/logo-classicblue-3000px.png
|
||||
url: https://intel.com/
|
||||
priority: 400
|
||||
topIcon: true
|
||||
topIconShowEvery: 1
|
||||
sideBySide: true
|
||||
statsId: intel
|
||||
- name: Backtrace
|
||||
@@ -29,7 +30,7 @@ levels:
|
||||
img: https://static.ce-cdn.net/bt/BT-logo (1).png
|
||||
url: https://backtrace.io/sign-up?utm_source=website&utm_medium=paiddisplay&utm_campaign=00O8Y000008JiuiUAC&utm_content=godboltbannerad
|
||||
priority: 500
|
||||
topIcon: true
|
||||
topIconShowEvery: 3
|
||||
sideBySide: true
|
||||
statsId: backtrace
|
||||
- name: Patreon Legends
|
||||
|
||||
@@ -31,7 +31,7 @@ export type Sponsor = {
|
||||
url?: string;
|
||||
onclick: string;
|
||||
priority: number;
|
||||
topIcon: boolean;
|
||||
topIconShowEvery: number;
|
||||
sideBySide: boolean;
|
||||
statsId?: string;
|
||||
};
|
||||
@@ -43,7 +43,10 @@ export type Level = {
|
||||
sponsors: Sponsor[];
|
||||
};
|
||||
|
||||
export type Sponsors = {
|
||||
icons: Sponsor[];
|
||||
levels: Level[];
|
||||
};
|
||||
export interface Sponsors {
|
||||
getLevels(): Level[];
|
||||
|
||||
pickTopIcons(): Sponsor[];
|
||||
|
||||
getAllTopIcons(): Sponsor[];
|
||||
}
|
||||
|
||||
109
lib/sponsors.ts
109
lib/sponsors.ts
@@ -24,7 +24,7 @@
|
||||
|
||||
import yaml from 'yaml';
|
||||
|
||||
import {Sponsor, Sponsors} from './sponsors.interfaces';
|
||||
import {Level, Sponsor, Sponsors} from './sponsors.interfaces';
|
||||
|
||||
export function parse(mapOrString: Record<string, any> | string): Sponsor {
|
||||
if (typeof mapOrString == 'string') mapOrString = {name: mapOrString};
|
||||
@@ -36,7 +36,7 @@ export function parse(mapOrString: Record<string, any> | string): Sponsor {
|
||||
img: mapOrString.img,
|
||||
icon: mapOrString.icon || mapOrString.img,
|
||||
icon_dark: mapOrString.icon_dark,
|
||||
topIcon: !!mapOrString.topIcon,
|
||||
topIconShowEvery: mapOrString.topIconShowEvery || 0,
|
||||
sideBySide: !!mapOrString.sideBySide,
|
||||
priority: mapOrString.priority || 0,
|
||||
statsId: mapOrString.statsId,
|
||||
@@ -50,14 +50,113 @@ function compareSponsors(lhs: Sponsor, rhs: Sponsor): number {
|
||||
return lhs.name.localeCompare(rhs.name);
|
||||
}
|
||||
|
||||
function calcMean(values: number[]): number {
|
||||
return values.reduce((x, y) => x + y, 0) / values.length;
|
||||
}
|
||||
|
||||
function squareSumFromMean(values: number[]): number {
|
||||
const mean = calcMean(values);
|
||||
return values.reduce((x, y) => x + (y - mean) * (y - mean), 0);
|
||||
}
|
||||
|
||||
function standardDeviation(values: number[]): number {
|
||||
return values.length < 2 ? 0 : Math.sqrt(squareSumFromMean(values) / (values.length - 1));
|
||||
}
|
||||
|
||||
// A sponsor icon set is ok if:
|
||||
// - each sponsor is shown at least every "topIconShowEvery"
|
||||
// - the standard deviation for the number of showings between sponsors at the same "show every' is not too high: that
|
||||
// is we fairly distribute showings of sponsors at the same level
|
||||
function sponsorIconSetsOk(
|
||||
sponsorAppearanceCount: Map<Sponsor, number>,
|
||||
totalAppearances: number,
|
||||
maxStandardDeviation: number,
|
||||
): boolean {
|
||||
const countsByShowEvery: Map<number, number[]> = new Map();
|
||||
for (const [icon, count] of sponsorAppearanceCount.entries()) {
|
||||
const seenEvery = count > 0 ? totalAppearances / count : Infinity;
|
||||
if (seenEvery > icon.topIconShowEvery) {
|
||||
return false;
|
||||
}
|
||||
const others = countsByShowEvery.get(icon.topIconShowEvery) || [];
|
||||
others.push(seenEvery);
|
||||
countsByShowEvery.set(icon.topIconShowEvery, others);
|
||||
}
|
||||
return Math.max(...[...countsByShowEvery.values()].map(standardDeviation)) <= maxStandardDeviation;
|
||||
}
|
||||
|
||||
export function makeIconSets(
|
||||
icons: Sponsor[],
|
||||
maxIcons: number,
|
||||
maxIters = 100,
|
||||
maxStandardDeviation = 0.5,
|
||||
): Sponsor[][] {
|
||||
const result: Sponsor[][] = [];
|
||||
const sponsorAppearanceCount: Map<Sponsor, number> = new Map();
|
||||
for (const icon of icons) sponsorAppearanceCount.set(icon, 0);
|
||||
while (!sponsorIconSetsOk(sponsorAppearanceCount, result.length, maxStandardDeviation)) {
|
||||
if (result.length > maxIters) {
|
||||
throw new Error(`Unable to find a solution in ${maxIters}`);
|
||||
}
|
||||
const toPick = icons.map(icon => {
|
||||
return {
|
||||
icon: icon,
|
||||
// Number of times we'd expect to see this, divided by number of times we saw it
|
||||
error: result.length / icon.topIconShowEvery / (sponsorAppearanceCount.get(icon) || 0.00001),
|
||||
};
|
||||
});
|
||||
toPick.sort((lhs, rhs) => rhs.error - lhs.error);
|
||||
const chosen = toPick
|
||||
.slice(0, maxIcons)
|
||||
.map(x => x.icon)
|
||||
.sort(compareSponsors);
|
||||
for (const c of chosen) sponsorAppearanceCount.set(c, (sponsorAppearanceCount.get(c) || 0) + 1);
|
||||
result.push(chosen);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class SponsorsImpl implements Sponsors {
|
||||
private readonly _levels: Level[];
|
||||
private readonly _icons: Sponsor[];
|
||||
private readonly _iconSets: Sponsor[][];
|
||||
private _nextSet: number;
|
||||
|
||||
constructor(levels: Level[], maxTopIcons) {
|
||||
this._levels = levels;
|
||||
this._icons = [];
|
||||
for (const level of levels) {
|
||||
this._icons.push(...level.sponsors.filter(sponsor => sponsor.topIconShowEvery && sponsor.icon));
|
||||
}
|
||||
this._iconSets = makeIconSets(this._icons, maxTopIcons);
|
||||
this._nextSet = 0;
|
||||
}
|
||||
|
||||
getLevels(): Level[] {
|
||||
return this._levels;
|
||||
}
|
||||
|
||||
getAllTopIcons(): Sponsor[] {
|
||||
return this._icons;
|
||||
}
|
||||
|
||||
pickTopIcons(): Sponsor[] {
|
||||
const result = this._iconSets[this._nextSet];
|
||||
this._nextSet = (this._nextSet + 1) % this._iconSets.length;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadSponsorsFromLevels(levels: Level[], maxTopIcons: number): Sponsors {
|
||||
return new SponsorsImpl(levels, maxTopIcons);
|
||||
}
|
||||
|
||||
export function loadSponsorsFromString(stringConfig: string): Sponsors {
|
||||
const sponsorConfig = yaml.parse(stringConfig);
|
||||
sponsorConfig.icons = [];
|
||||
for (const level of sponsorConfig.levels) {
|
||||
for (const required of ['name', 'description', 'sponsors'])
|
||||
if (!level[required]) throw new Error(`Level is missing '${required}'`);
|
||||
level.sponsors = level.sponsors.map(parse).sort(compareSponsors);
|
||||
sponsorConfig.icons.push(...level.sponsors.filter(sponsor => sponsor.topIcon && sponsor.icon));
|
||||
}
|
||||
return sponsorConfig;
|
||||
return loadSponsorsFromLevels(sponsorConfig.levels, sponsorConfig.maxTopIcons || 3);
|
||||
}
|
||||
|
||||
@@ -196,6 +196,14 @@ function setupButtons(options, hub) {
|
||||
alertSystem.alert('Changelog', $(require('./generated/changelog.pug').default.text));
|
||||
});
|
||||
|
||||
$.get(window.location.origin + window.httpRoot + 'bits/icons.html')
|
||||
.done(function (data) {
|
||||
$('#ces .ces-icons').html(data);
|
||||
})
|
||||
.fail(function (err) {
|
||||
Sentry.captureException(err);
|
||||
});
|
||||
|
||||
$('#ces').on('click', function () {
|
||||
$.get(window.location.origin + window.httpRoot + 'bits/sponsors.html')
|
||||
.done(function (data) {
|
||||
|
||||
@@ -22,9 +22,11 @@
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import {loadSponsorsFromString, parse} from '../lib/sponsors';
|
||||
import fs from 'fs';
|
||||
|
||||
import {should} from './utils';
|
||||
import {loadSponsorsFromString, makeIconSets, parse} from '../lib/sponsors';
|
||||
|
||||
import {resolvePathFromTestRoot, should} from './utils';
|
||||
|
||||
describe('Sponsors', () => {
|
||||
it('should expand names to objects', () => {
|
||||
@@ -41,7 +43,7 @@ describe('Sponsors', () => {
|
||||
should.equal(obj.img, undefined);
|
||||
should.equal(obj.icon, undefined);
|
||||
should.equal(obj.icon_dark, undefined);
|
||||
obj.topIcon.should.be.false;
|
||||
obj.topIconShowEvery.should.eq(0);
|
||||
obj.sideBySide.should.be.false;
|
||||
should.equal(obj.statsId, undefined);
|
||||
});
|
||||
@@ -64,7 +66,7 @@ describe('Sponsors', () => {
|
||||
parse({name: 'bob', icon: 'icon', icon_dark: 'icon_dark'}).icon_dark.should.eq('icon_dark');
|
||||
});
|
||||
it('should handle topIcons', () => {
|
||||
parse({name: 'bob', topIcon: true}).topIcon.should.be.true;
|
||||
parse({name: 'bob', topIconShowEvery: 2}).topIconShowEvery.should.eq(2);
|
||||
});
|
||||
it('should handle clicks', () => {
|
||||
parse({
|
||||
@@ -92,9 +94,10 @@ levels:
|
||||
- Yay
|
||||
`);
|
||||
sample.should.not.be.null;
|
||||
sample.levels.length.should.eq(2);
|
||||
sample.levels[0].name.should.eq('Patreon Legends');
|
||||
sample.levels[1].name.should.eq('Patreons');
|
||||
const levels = sample.getLevels();
|
||||
levels.length.should.eq(2);
|
||||
levels[0].name.should.eq('Patreon Legends');
|
||||
levels[1].name.should.eq('Patreons');
|
||||
});
|
||||
|
||||
it('should sort sponsors by name', () => {
|
||||
@@ -108,7 +111,7 @@ levels:
|
||||
- C
|
||||
- A
|
||||
- B
|
||||
`).levels[0].sponsors;
|
||||
`).getLevels()[0].sponsors;
|
||||
peeps.map(sponsor => sponsor.name).should.deep.equals(['A', 'B', 'C', 'D']);
|
||||
});
|
||||
it('should sort sponsors by priority then name', () => {
|
||||
@@ -124,7 +127,7 @@ levels:
|
||||
priority: 50
|
||||
- name: B
|
||||
priority: 50
|
||||
`).levels[0].sponsors;
|
||||
`).getLevels()[0].sponsors;
|
||||
peeps
|
||||
.map(sponsor => {
|
||||
return {name: sponsor.name, priority: sponsor.priority};
|
||||
@@ -136,7 +139,7 @@ levels:
|
||||
]);
|
||||
});
|
||||
|
||||
it('should pick out the top level icons', () => {
|
||||
it('should pick out all the top level icons', () => {
|
||||
const icons = loadSponsorsFromString(`
|
||||
---
|
||||
levels:
|
||||
@@ -145,7 +148,7 @@ levels:
|
||||
sponsors:
|
||||
- name: one
|
||||
img: pick_me
|
||||
topIcon: true
|
||||
topIconShowEvery: 1
|
||||
- name: two
|
||||
img: not_me
|
||||
- name: another level
|
||||
@@ -153,13 +156,96 @@ levels:
|
||||
sponsors:
|
||||
- name: three
|
||||
img: not_me_either
|
||||
topIcon: false
|
||||
topIconShowEvery: 0
|
||||
- name: four
|
||||
img: pick_me_also
|
||||
topIcon: true
|
||||
topIconShowEvery: 2
|
||||
- name: five
|
||||
topIcon: true
|
||||
`).icons;
|
||||
topIconShowEvery: 3
|
||||
`).getAllTopIcons();
|
||||
icons.map(s => s.name).should.deep.equals(['one', 'four']);
|
||||
});
|
||||
|
||||
it('should pick icons appropriately when all required every 3', () => {
|
||||
const sponsor1 = parse({name: 'Sponsor1', topIconShowEvery: 3, icon: '1'});
|
||||
const sponsor2 = parse({name: 'Sponsor2', topIconShowEvery: 3, icon: '2'});
|
||||
const sponsor3 = parse({name: 'Sponsor3', topIconShowEvery: 3, icon: '3'});
|
||||
const icons = [sponsor1, sponsor2, sponsor3];
|
||||
makeIconSets(icons, 10).should.deep.eq([icons]);
|
||||
makeIconSets(icons, 3).should.deep.eq([icons]);
|
||||
makeIconSets(icons, 2).should.deep.eq([
|
||||
[sponsor1, sponsor2],
|
||||
[sponsor1, sponsor3],
|
||||
[sponsor2, sponsor3],
|
||||
]);
|
||||
makeIconSets(icons, 1).should.deep.eq([[sponsor1], [sponsor2], [sponsor3]]);
|
||||
});
|
||||
it('should pick icons appropriately when not required on different schedules', () => {
|
||||
const sponsor1 = parse({name: 'Sponsor1', topIconShowEvery: 1, icon: '1'});
|
||||
const sponsor2 = parse({name: 'Sponsor2', topIconShowEvery: 2, icon: '2'});
|
||||
const sponsor3 = parse({name: 'Sponsor3', topIconShowEvery: 3, icon: '3'});
|
||||
const icons = [sponsor1, sponsor2, sponsor3];
|
||||
makeIconSets(icons, 10).should.deep.eq([icons]);
|
||||
makeIconSets(icons, 3).should.deep.eq([icons]);
|
||||
makeIconSets(icons, 2).should.deep.eq([
|
||||
[sponsor1, sponsor2],
|
||||
[sponsor1, sponsor3],
|
||||
]);
|
||||
(() => makeIconSets(icons, 1)).should.throw();
|
||||
});
|
||||
it('should pick icons appropriately with a lot of sponsors on representative schedules', () => {
|
||||
const sponsor1 = parse({name: 'Sponsor1', topIconShowEvery: 1, icon: '1'});
|
||||
const sponsor2 = parse({name: 'Sponsor2', topIconShowEvery: 3, icon: '2'});
|
||||
const sponsor3 = parse({name: 'Sponsor3', topIconShowEvery: 3, icon: '3'});
|
||||
const sponsor4 = parse({name: 'Sponsor4', topIconShowEvery: 3, icon: '3'});
|
||||
const sponsor5 = parse({name: 'Sponsor5', topIconShowEvery: 3, icon: '3'});
|
||||
const icons = [sponsor1, sponsor2, sponsor3, sponsor4, sponsor5];
|
||||
makeIconSets(icons, 10).should.deep.eq([icons]);
|
||||
makeIconSets(icons, 3).should.deep.eq([
|
||||
[sponsor1, sponsor2, sponsor3],
|
||||
[sponsor1, sponsor4, sponsor5],
|
||||
]);
|
||||
(() => makeIconSets(icons, 1)).should.throw();
|
||||
});
|
||||
it('should handle alternating', () => {
|
||||
const sponsor1 = parse({name: 'Sponsor1', topIconShowEvery: 1, icon: '1'});
|
||||
const sponsor2 = parse({name: 'Sponsor2', topIconShowEvery: 1, icon: '2'});
|
||||
const sponsor3 = parse({name: 'Sponsor3', topIconShowEvery: 2, icon: '3'});
|
||||
const sponsor4 = parse({name: 'Sponsor4', topIconShowEvery: 2, icon: '4'});
|
||||
const icons = [sponsor1, sponsor2, sponsor3, sponsor4];
|
||||
makeIconSets(icons, 4).should.deep.eq([icons]);
|
||||
makeIconSets(icons, 3).should.deep.eq([
|
||||
[sponsor1, sponsor2, sponsor3],
|
||||
[sponsor1, sponsor2, sponsor4],
|
||||
]);
|
||||
(() => makeIconSets(icons, 2)).should.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Our specific sponsor file', () => {
|
||||
const stringConfig = fs.readFileSync(resolvePathFromTestRoot('../etc/config/sponsors.yaml')).toString();
|
||||
it('should parse the current config', () => {
|
||||
loadSponsorsFromString(stringConfig);
|
||||
});
|
||||
it('should pick appropriate sponsor icons', () => {
|
||||
const numLoads = 100;
|
||||
const expectedNumIcons = 3;
|
||||
|
||||
const sponsors = loadSponsorsFromString(stringConfig);
|
||||
const picks = [];
|
||||
for (let load = 0; load < numLoads; ++load) {
|
||||
picks.push(sponsors.pickTopIcons());
|
||||
}
|
||||
const countBySponsor = new Map();
|
||||
for (const pick of picks) {
|
||||
for (const sponsor of pick) {
|
||||
countBySponsor.set(sponsor, (countBySponsor.get(sponsor) || 0) + 1);
|
||||
}
|
||||
pick.length.should.eq(expectedNumIcons);
|
||||
}
|
||||
for (const topIcon of sponsors.getAllTopIcons()) {
|
||||
const appearsEvery = countBySponsor.get(topIcon) / numLoads;
|
||||
appearsEvery.should.lte(topIcon.topIconShowEvery);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
6
views/bits/icons.pug
Normal file
6
views/bits/icons.pug
Normal file
@@ -0,0 +1,6 @@
|
||||
each icon in sponsors.pickTopIcons()
|
||||
if !icon.icon_dark || icon.icon === icon.icon_dark
|
||||
img.ces-icon(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||
else
|
||||
img.ces-icon.theme-light-only(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||
img.ces-icon.theme-dark-only(src=icon.icon_dark alt=icon.name data-statsid=icon.statsId)
|
||||
@@ -18,7 +18,7 @@ block content
|
||||
a(href="mailto:matt@godbolt.org") email Matt
|
||||
| for corporate sponsorship.
|
||||
.mt-6
|
||||
each level, index in sponsors.levels
|
||||
each level, index in sponsors.getLevels()
|
||||
.ces-item-block(class=level.class id="ces_hop_" + index)
|
||||
h2.ces-item-name= level.name
|
||||
div.ces-item-description= level.description
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
div.ces-level-selectors.d-flex.justify-content-center.mt-3
|
||||
div.btn-group.btn-group
|
||||
each level, index in sponsors.levels
|
||||
each level, index in sponsors.getLevels()
|
||||
button.btn.btn-primary(onclick='document.getElementById("ces_hop_' + index + '").scrollIntoView({behavior:"smooth"});')=level.name
|
||||
|
||||
block content
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
.ces-icons
|
||||
each icon in sponsors.icons
|
||||
if noscript
|
||||
a(href=icon.url)
|
||||
img.ces-icon(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||
span
|
||||
else
|
||||
if !icon.icon_dark || icon.icon === icon.icon_dark
|
||||
img.ces-icon(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||
else
|
||||
img.ces-icon.theme-light-only(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||
img.ces-icon.theme-dark-only(src=icon.icon_dark alt=icon.name data-statsid=icon.statsId)
|
||||
if noscript
|
||||
each icon in sponsors.pickTopIcons()
|
||||
a(href=icon.url)
|
||||
img.ces-icon(src=icon.icon alt=icon.name data-statsid=icon.statsId)
|
||||
span
|
||||
|
||||
Reference in New Issue
Block a user