Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

P95 #1008

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft

P95 #1008

Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions components/page/multiSelectLevel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Level from '@root/models/db/level';
import React, { useState } from 'react';
import AsyncSelect from 'react-select/async';
import { debounce } from 'throttle-debounce';

interface MultiSelectLevelProps {
controlStyles?: any;
defaultValue?: Level | null;
onSelect?: (selectedList: any, selectedItem: any) => void;
placeholder?: string
}

export default function MultiSelectLevel({ controlStyles, defaultValue, onSelect, placeholder }: MultiSelectLevelProps) {
const [options, setOptions] = useState([]);
const [value, setValue] = useState(defaultValue);

const doSearch = async (searchText: any, callback: any) => {
const search = encodeURI(searchText) || '';

const res = await fetch('/api/search?search=' + search, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});

const data = await res.json();

setOptions(data?.levels);
callback(data?.levels);
};
const debounceDoSearch = debounce(500, doSearch);

return <AsyncSelect
backspaceRemovesValue={true}
className='text-left text-base'
components={{
DropdownIndicator: null,
IndicatorSeparator: null,
}}
formatOptionLabel={(option: any) => (
<>{option.name}</>
)}
getOptionLabel={(option: any) => option.name}
getOptionValue={(option: any) => option._id.toString()}
id='search-author-input-async-select'
instanceId='search-author-input-async-select'
isClearable={true}
loadOptions={debounceDoSearch}
noOptionsMessage={() => 'No levels found'}
onChange={(selectedOption: any, selectedAction: any) => {
setValue(selectedOption);

if (!selectedAction) {
return;
}

if (onSelect) {
if (selectedAction.action === 'clear') {
onSelect('', selectedAction);
} else {
onSelect(selectedOption, selectedAction);
}
}
}}
options={options} // Options to display in the dropdown
placeholder={placeholder ? placeholder : 'Search levels...'}
// https://react-select.com/styles
styles={{
control: (provided: any, state: any) => ({
...provided,
backgroundColor: 'white',
borderColor: state.isFocused ? 'rgb(37 99 235)' : 'rgb(209 213 219)',
borderRadius: '0.375rem',
borderWidth: '1px',
boxShadow: 'none',
cursor: 'text',
height: '2.5rem',
width: '13rem',
...controlStyles,
}),
dropdownIndicator: (provided: any) => ({
...provided,
color: 'black',
// change to search icon
'&:hover': {
color: 'gray',
},
}),
input: (provided: any) => ({
...provided,
color: 'rgb(55 65 81)',
}),
menu: (provided: any) => ({
...provided,
borderColor: 'rgb(209 213 219)',
borderRadius: '0.375rem',
borderWidth: '1px',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
marginTop: '2px',
}),
option: (provided: any, state: any) => ({
...provided,
backgroundColor: state.isSelected ? '#e2e8f0' : 'white',
color: 'black',
'&:hover': {
backgroundColor: '#e2e8f0',
},
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
placeholder: (provided: any) => ({
...provided,
color: 'rgb(156 163 175)',
}),
singleValue: (provided: any) => ({
...provided,
color: 'black',
}),
}}
value={value}
/>;
}
2 changes: 2 additions & 0 deletions models/db/level.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ interface Level {
authorNote?: string;
calc_difficulty_estimate: number;
calc_playattempts_duration_sum: number;
calc_playattempts_duration_sum_p95: number;
calc_playattempts_just_beaten_count: number;
calc_playattempts_just_beaten_count_p95: number;
calc_playattempts_unique_users: Types.ObjectId[];
calc_reviews_count: number;
calc_reviews_score_avg: number;
Expand Down
171 changes: 112 additions & 59 deletions models/schemas/levelSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const LEVEL_SEARCH_DEFAULT_PROJECTION = { _id: 1, ts: 1, name: 1, slug: 1

const LevelSchema = new mongoose.Schema<Level>(
{

archivedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
Expand All @@ -31,10 +30,18 @@ const LevelSchema = new mongoose.Schema<Level>(
type: Number,
default: 0,
},
calc_playattempts_duration_sum_p95: {
type: Number,
default: 0,
},
calc_playattempts_just_beaten_count: {
type: Number,
default: 0,
},
calc_playattempts_just_beaten_count_p95: {
type: Number,
default: 0,
},
calc_playattempts_unique_users: {
type: [mongoose.Schema.Types.ObjectId],
default: [],
Expand Down Expand Up @@ -114,7 +121,7 @@ const LevelSchema = new mongoose.Schema<Level>(
locale: 'en_US',
strength: 2,
},
}
},
);

LevelSchema.index({ slug: 1 }, { name: 'slug_index', unique: true });
Expand All @@ -126,6 +133,8 @@ LevelSchema.index({ isDraft: 1 });
LevelSchema.index({ leastMoves: 1 });
LevelSchema.index({ calc_difficulty_estimate: 1 });
LevelSchema.index({ calc_playattempts_duration_sum: 1 });
LevelSchema.index({ calc_playattempts_duration_sum_p95: 1 });
LevelSchema.index({ calc_playattempts_just_beaten_count_p95: 1 });
LevelSchema.index({ calc_playattempts_just_beaten_count: 1 });
LevelSchema.index({ calc_playattempts_unique_users: 1 });
LevelSchema.index({ calc_reviews_count: 1 });
Expand Down Expand Up @@ -200,78 +209,122 @@ async function calcStats(lvl: Level) {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function calcPlayAttempts(levelId: Types.ObjectId, options: any = {}) {
const countJustBeaten = await PlayAttemptModel.countDocuments({
levelId: levelId,
attemptContext: AttemptContext.JUST_BEATEN,
});

// sumDuration is all of the sum(endTime-startTime) within the playAttempts
const sumDuration = await PlayAttemptModel.aggregate([
{
$match: {
levelId: levelId,
attemptContext: { $ne: AttemptContext.BEATEN },
const [countJustBeaten, sumDuration, sumDurationP95, uniqueUsersList] = await Promise.all([
PlayAttemptModel.countDocuments({
levelId: levelId,
attemptContext: AttemptContext.JUST_BEATEN,
}),
PlayAttemptModel.aggregate([
{
$match: {
levelId: levelId,
attemptContext: { $ne: AttemptContext.BEATEN },
}
},
{
$group: {
_id: null,
sumDuration: {
$sum: {
$subtract: ['$endTime', '$startTime']
}
}
}
}
},
{
$group: {
_id: null,
sumDuration: {
$sum: {
], options),
PlayAttemptModel.aggregate([
{
$match: {
levelId: levelId,
attemptContext: { $ne: AttemptContext.BEATEN }
}
},
{
$project: {
duration: {
$subtract: ['$endTime', '$startTime']
}
}
},
{ $sort: { duration: 1 } },
{
$group: {
_id: null,
durations: { $push: '$duration' },
count: { $sum: 1 }
}
},
{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we combine sumDuration and sumDurationP95? Should have all the data for sumDuration at this point

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah good idea

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually i realized when trying to do this that this is risky and not actually as straightforward since i do a bunch of groups. probably safer for now to keep it separate

$project: {
lowerIndex: { $ceil: { $multiply: ['$count', 0.025] } },
upperIndex: { $floor: { $multiply: ['$count', 0.975] } },
durations: 1,
count: 1
}
},
{
$project: {
countExcluded: { $subtract: ['$count', { $subtract: ['$upperIndex', '$lowerIndex'] }] },
sumExcluded: {
$reduce: {
input: { $slice: ['$durations', '$lowerIndex', { $subtract: ['$upperIndex', '$lowerIndex'] }] },
initialValue: 0,
in: { $add: ['$$value', '$$this'] }
}
}
}
}
}
], options);

// get array of unique userIds from playattempt calc_playattempts_unique_users
const uniqueUsersList = await PlayAttemptModel.aggregate([
{
$match: {
$or: [
{
$and: [
{
$expr: {
$gt: [
{
$subtract: ['$endTime', '$startTime']
},
0
]
]),
PlayAttemptModel.aggregate([
{
$match: {
$or: [
{
$and: [
{
$expr: {
$gt: [
{
$subtract: ['$endTime', '$startTime']
},
0
]
}
},
{
attemptContext: AttemptContext.UNBEATEN,
}
},
{
attemptContext: AttemptContext.UNBEATEN,
}
],
},
{
attemptContext: AttemptContext.JUST_BEATEN,
],
},
{
attemptContext: AttemptContext.JUST_BEATEN,
},
],
levelId: levelId,
},
},
{
$group: {
_id: null,
userId: {
$addToSet: '$userId',
},
],
levelId: levelId,
}
},
},
{
$group: {
_id: null,
userId: {
$addToSet: '$userId',
{
$unwind: {
path: '$userId',
preserveNullAndEmptyArrays: true,
},
}
},
{
$unwind: {
path: '$userId',
preserveNullAndEmptyArrays: true,
},
},
])
]);

const update = {
calc_playattempts_duration_sum: sumDuration[0]?.sumDuration ?? 0,
calc_playattempts_duration_sum_p95: sumDurationP95[0]?.sumExcluded ?? 0,
calc_playattempts_just_beaten_count_p95: sumDurationP95[0]?.countExcluded ?? 0,
k2xl marked this conversation as resolved.
Show resolved Hide resolved
calc_playattempts_just_beaten_count: countJustBeaten,
calc_playattempts_unique_users: uniqueUsersList.map(u => u?.userId.toString()),
} as Partial<Level>;
Expand Down
Loading