瀏覽代碼

Add daily/monthly download stats

Jordi Boggiano 12 年之前
父節點
當前提交
ddec84cfd0

+ 147 - 0
src/Packagist/WebBundle/Command/CompileStatsCommand.php

@@ -0,0 +1,147 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Command;
+
+use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Output\OutputInterface;
+use Packagist\WebBundle\Package\Updater;
+use Composer\Repository\VcsRepository;
+use Composer\Factory;
+use Composer\Package\Loader\ValidatingArrayLoader;
+use Composer\Package\Loader\ArrayLoader;
+use Composer\IO\BufferIO;
+use Composer\IO\ConsoleIO;
+use Composer\Repository\InvalidRepositoryException;
+
+/**
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class CompileStatsCommand extends ContainerAwareCommand
+{
+    protected $redis;
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this
+            ->setName('packagist:stats:compile')
+            ->setDefinition(array(
+                new InputOption('force', null, InputOption::VALUE_NONE, 'Force a re-build of all stats'),
+            ))
+            ->setDescription('Updates the redis stats indices')
+        ;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $verbose = $input->getOption('verbose');
+        $force = $input->getOption('force');
+
+        $doctrine = $this->getContainer()->get('doctrine');
+        $this->redis = $redis = $this->getContainer()->get('snc_redis.default');
+
+        $minMax = $doctrine->getEntityManager()->getConnection()->fetchAssoc('SELECT MAX(id) maxId, MIN(id) minId FROM package');
+        if (!isset($minMax['minId'])) {
+            return 0;
+        }
+
+        $ids = range($minMax['minId'], $minMax['maxId']);
+        $res = $doctrine->getEntityManager()->getConnection()->fetchAssoc('SELECT MIN(createdAt) minDate FROM package');
+        $date = new \DateTime($res['minDate']);
+        $date->modify('00:00:00');
+        $yesterday = new \DateTime('yesterday 00:00:00');
+
+        if ($force) {
+            if ($verbose) {
+                $output->writeln('Clearing aggregated DB');
+            }
+            $clearDate = clone $date;
+            $keys = array();
+            while ($clearDate <= $yesterday) {
+                $keys['downloads:'.$clearDate->format('Ymd')] = true;
+                $keys['downloads:'.$clearDate->format('Ym')] = true;
+                $clearDate->modify('+1day');
+            }
+            $redis->del(array_keys($keys));
+        }
+
+        while ($date <= $yesterday) {
+            // skip months already computed
+            if (null !== $this->getMonthly($date) && $date->format('m') !== $yesterday->format('m')) {
+                $date->setDate($date->format('Y'), $date->format('m')+1, 1);
+                continue;
+            }
+
+            // skip days already computed
+            if (null !== $this->getDaily($date) && $date != $yesterday) {
+                $date->modify('+1day');
+                continue;
+            }
+
+            $sum = $this->sum($date->format('Ymd'), $ids);
+            $redis->set('downloads:'.$date->format('Ymd'), $sum);
+
+            if ($verbose) {
+                $output->writeln('Wrote daily data for '.$date->format('Y-m-d').': '.$sum);
+            }
+
+            $nextDay = clone $date;
+            $nextDay->modify('+1day');
+            // update the monthly total if we just computed the last day of the month or the last known day
+            if ($date->format('Ymd') === $yesterday->format('Ymd') || $date->format('Ym') !== $nextDay->format('Ym')) {
+                $sum = $this->sum($date->format('Ym'), $ids);
+                $redis->set('downloads:'.$date->format('Ym'), $sum);
+
+                if ($verbose) {
+                    $output->writeln('Wrote monthly data for '.$date->format('Y-m').': '.$sum);
+                }
+            }
+
+            $date = $nextDay;
+        }
+    }
+
+    protected function sum($date, array $ids)
+    {
+        $sum = 0;
+
+        while ($ids) {
+            $batch = array_splice($ids, 0, 500);
+            $keys = array();
+            foreach ($batch as $id) {
+                $keys[] = 'dl:'.$id.':'.$date;
+            }
+            $sum += array_sum($res = $this->redis->mget($keys));
+        }
+
+        return $sum;
+    }
+
+    protected function getMonthly(\DateTime $date)
+    {
+        return $this->redis->get('downloads:'.$date->format('Ym'));
+    }
+
+    protected function getDaily(\DateTime $date)
+    {
+        return $this->redis->get('downloads:'.$date->format('Ymd'));
+    }
+}

+ 32 - 1
src/Packagist/WebBundle/Controller/WebController.php

@@ -620,14 +620,40 @@ class WebController extends Controller
             $chart['packages'] += array_fill(0, count($chart['months']) - count($chart['packages']), max($chart['packages']));
         }
         if (count($chart['months']) > count($chart['versions'])) {
-           $chart['versions'] += array_fill(0, count($chart['months']) - count($chart['versions']), max($chart['versions']));
+            $chart['versions'] += array_fill(0, count($chart['months']) - count($chart['versions']), max($chart['versions']));
         }
 
+
+        $res = $this->getDoctrine()
+            ->getConnection()
+            ->fetchAssoc('SELECT DATE_FORMAT(createdAt, "%Y-%m-%d") createdAt FROM `package` ORDER BY id LIMIT 1');
+        $downloadsStartDate = $res['createdAt'] > '2012-04-13' ? $res['createdAt'] : '2012-04-13';
+
         try {
             $redis = $this->get('snc_redis.default');
             $downloads = $redis->get('downloads') ?: 0;
+
+            $date = new \DateTime($downloadsStartDate.' 00:00:00');
+            $yesterday = new \DateTime('-2days 00:00:00');
+
+            $dlChart = $dlChartMonthly = array();
+            while ($date <= $yesterday) {
+                $dlChart[$date->format('Y-m-d')] = 'downloads:'.$date->format('Ymd');
+                $dlChartMonthly[$date->format('Y-m')] = 'downloads:'.$date->format('Ym');
+                $date->modify('+1day');
+            }
+
+            $dlChart = array(
+                'labels' => array_keys($dlChartMonthly),
+                'values' => $redis->mget(array_values($dlChart))
+            );
+            $dlChartMonthly = array(
+                'labels' => array_keys($dlChartMonthly),
+                'values' => $redis->mget(array_values($dlChartMonthly))
+            );
         } catch (\Exception $e) {
             $downloads = 'N/A';
+            $dlChart = null;
         }
 
         return array(
@@ -635,6 +661,11 @@ class WebController extends Controller
             'packages' => max($chart['packages']),
             'versions' => max($chart['versions']),
             'downloads' => $downloads,
+            'downloadsChart' => $dlChart,
+            'maxDailyDownloads' => !empty($dlChart) ? max($dlChart['values']) : null,
+            'downloadsChartMonthly' => $dlChartMonthly,
+            'maxMonthlyDownloads' => !empty($dlChartMonthly) ? max($dlChartMonthly['values']) : null,
+            'downloadsStartDate' => $downloadsStartDate,
         );
     }
 

+ 11 - 1
src/Packagist/WebBundle/Resources/views/Web/stats.html.twig

@@ -8,9 +8,19 @@
         <p><img src="http://chart.apis.google.com/chart?chxr=0,0,{{ versions }}&amp;chxl=1:|{{ chart.months|join('|') }}&amp;chxt=y,x&amp;chs=900x250&amp;chds=0,{{ versions }},0,{{ versions }}&amp;cht=lc&amp;chco=0000FF,FF9900&amp;chd=t:{{ chart.versions|join(',') }}|{{ chart.packages|join(',') }}&amp;chdl=Versions|Packages&amp;chls=2|2" /></p>
         <p>The last data point is for the current month and shows partial data.</p>
 
+        {% if downloadsChart %}
+            <h2>Packages installed per day</h2>
+            <p><img src="http://chart.apis.google.com/chart?chxr=0,0,{{ maxDailyDownloads }}&amp;chxl=1:|{{ downloadsChart.labels|join('|') }}&amp;chxt=y,x&amp;chs=900x250&amp;chds=0,{{ maxDailyDownloads }},0,{{ maxDailyDownloads }}&amp;cht=lc&amp;chco=0000FF,FF9900&amp;chd=t:{{ downloadsChart.values|join(',') }}&amp;chdl=Installs&amp;chls=2|2" /></p>
+        {% endif %}
+        {% if downloadsChartMonthly %}
+            <h2>Packages installed per month</h2>
+            <p><img src="http://chart.apis.google.com/chart?chxr=0,0,{{ maxMonthlyDownloads }}&amp;chxl=1:|{{ downloadsChartMonthly.labels|join('|') }}&amp;chxt=y,x&amp;chs=900x250&amp;chds=0,{{ maxMonthlyDownloads }},0,{{ maxMonthlyDownloads }}&amp;cht=lc&amp;chco=0000FF,FF9900&amp;chd=t:{{ downloadsChartMonthly.values|join(',') }}&amp;chdl=Installs&amp;chls=2|2" /></p>
+        {% endif %}
+        <p>The last data point is for the current month and shows partial data.</p>
+
         <h2>Totals</h2>
         <p>{{ packages|number_format(0, '.', " ") }} packages registered</p>
         <p>{{ versions|number_format(0, '.', " ") }} versions available</p>
-        <p>{{ downloads == 'N/A' ? downloads : downloads|number_format(0, '.', " ") }} packages installed (since 2012-04-13)</p>
+        <p>{{ downloads == 'N/A' ? downloads : downloads|number_format(0, '.', " ") }} packages installed (since {{ downloadsStartDate }})</p>
     </div>
 {% endblock %}