Problem/Motivation

MySQL doesn't perform well enough for this kind of thing

Proposed resolution

Move all database interaction to a service.

Remaining tasks

None

User interface changes

None

API changes

  • All database interactions to be moved to NodeStatisticsDatabaseStorage
  • update statistics_get() is deprecated, use \Drupal::service('statistics.storage.node')->fetchView($id) instead.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task
Issue priority Normal
Unfrozen changes Unfrozen because it helps with migration for node counters.
Prioritized changes The main goal of this issue is performance. Previously Statistics has had a bad name because of the number of database reads and writes. This patch allows statistics to move to Mongo, Redis, Couch or another fast data store. This patch also provides a clean way to access the storage which can be used by migration.
Disruption None
CommentFileSizeAuthor
#216 improve_statistics-1446932-216.patch20.86 KBjibran
#215 improve_statistics-1446932-215.patch21.02 KBjibran
#199 interdiff.txt1.92 KBtimmillwood
#199 improve_statistics-1446932-199.patch20.8 KBtimmillwood
#196 interdiff.txt5.22 KBtimmillwood
#196 improve_statistics-1446932-196.patch23.63 KBtimmillwood
#188 interdiff.txt4.11 KBmallezie
#188 improve_statistics-1446932-188.patch20.77 KBmallezie
#181 interdiff.txt510 bytesmallezie
#181 improve_statistics-1446932-181.patch20.79 KBmallezie
#180 interdiff.txt8.65 KBmallezie
#180 improve_statistics-1446932-180.patch20.82 KBmallezie
#177 interdiff.txt6.06 KBmallezie
#177 improve_statistics-1446932-177.patch20.8 KBmallezie
#173 interdiff.txt1.82 KBmallezie
#173 improve_statistics-1446932-173.patch20.67 KBmallezie
#171 interdiff.txt556 bytesmallezie
#171 improve_statistics-1446932-171.patch20.5 KBmallezie
#169 interdiff.txt7.93 KBmallezie
#169 improve_statistics-1446932-169.patch20.5 KBmallezie
#166 interdiff.txt597 bytesmallezie
#166 improve_statistics-1446932-166.patch26.89 KBmallezie
#164 interdiff.txt15.44 KBmallezie
#164 improve_statistics-1446932-164.patch26.89 KBmallezie
#158 improve_statistics-1446932-158.patch32.59 KBtimmillwood
#158 interdiff.txt729 bytestimmillwood
#156 interdiff.txt2.9 KBtimmillwood
#156 improve_statistics-1446932-156.patch32.61 KBtimmillwood
#153 improve_statistics-1446932-153.patch33.04 KBtimmillwood
#153 interdiff.txt635 bytestimmillwood
#149 improve_statistics-1446932-149.patch36.07 KBtimmillwood
#149 interdiff.txt13.9 KBtimmillwood
#144 improve_statistics-1446932-144.patch27.03 KBtimmillwood
#144 interdiff.txt8.38 KBtimmillwood
#143 interdiff-139-141.txt5.44 KBmparker17
#141 improve_statistics-1446932-141.patch24.95 KBtimmillwood
#139 interdiff.txt9.57 KBtimmillwood
#139 improve_statistics-1446932-139.patch28.34 KBtimmillwood
#134 improve_statistics-1446932-134.patch27.98 KBkostyashupenko
#124 interdiff.txt1.2 KBWim Leers
#124 improve_statistics-1446932-124.patch27.8 KBWim Leers
#121 improve_statistics-1446932-121.patch28.16 KBhussainweb
#119 improve_statistics-1446932-119.patch28.58 KBtimmillwood
#119 interdiff-115-119.txt1.77 KBtimmillwood
#115 improve_statistics-1446932-115.patch28.47 KBtimmillwood
#115 interdiff-111-115.txt6.17 KBtimmillwood
#111 improve_statistics-1446932-111.patch28.43 KBhussainweb
#111 interdiff-110-111.txt687 byteshussainweb
#110 improve_statistics-1446932-110.patch28.5 KBtimmillwood
#110 interdiff-98-110.txt19.84 KBtimmillwood
#107 interdiff-98-107.txt15.18 KBtimmillwood
#107 improve_statistics-1446932-107.patch26.44 KBtimmillwood
#100 giphy.gif1008.63 KBtimmillwood
#98 improve_statistics-1446932-98.patch18.04 KBtimmillwood
#98 interdiff-95-98.txt1.69 KBtimmillwood
#95 improve_statistics-1446932-95.patch17.71 KBtimmillwood
#94 improve_statistics-1446932-94.patch17.73 KBtimmillwood
#94 interdiff-92-94.txt2.33 KBtimmillwood
#92 improve_statistics-1446932-92.patch17.71 KBtimmillwood
#92 interdiff-88-92.txt4 KBtimmillwood
#88 improve_statistics-1446932-88.patch16.92 KBtimmillwood
#86 improve_statistics-1446932-86.patch16 KBtimmillwood
#86 interdiff-84-86.txt1.94 KBtimmillwood
#84 improve_statistics-1446932-84.patch15.9 KBtimmillwood
#82 interdiff-79-82.txt3.67 KBtimmillwood
#82 improve_statistics-1446932-82.patch15.9 KBtimmillwood
#79 improve_statistics-1446932-79.patch15.53 KBtimmillwood
#79 interdiff-75-79.txt15.98 KBtimmillwood
#75 improve_statistics-1446932-75.patch12.62 KBtimmillwood
#72 improve_statistics-1446932-72.patch9.51 KBtimmillwood
#69 improve_statistics-1446932-69.patch12.44 KBtimmillwood
#68 improve_statistics-1446932-68.patch9.86 KBtimmillwood
#62 improve_statistics-1446932-62.patch9.92 KBtimmillwood
#61 improve_statistics-1446932-61.patch0 bytestimmillwood
#59 1446932-59.patch7.24 KBrpayanm
#57 improve_statistics-1446932-57.patch7.24 KBtimmillwood
#55 improve_statistics-1446932-55.patch3.04 KBtimmillwood
#50 improve_statistics-1446932-50.patch2.33 KBtimmillwood
#42 1446932_6.patch5.07 KBtimmillwood
#39 1446932_5.patch5.19 KBtimmillwood
#38 1446932_4.patch4.58 KBtimmillwood
#36 1446932_3.patch4.58 KBtimmillwood
#34 1446932_2.patch4.09 KBtimmillwood
#31 1446932.patch3.18 KBRobLoach
#29 1446932.patch3.18 KBRobLoach
#28 1446932.patch3.18 KBRobLoach
#27 statsback2.patch4.28 KBtimmillwood
#26 statsback.patch4 KBtimmillwood
#24 statistics-queue-1446932-24.patch13.89 KBtimmillwood
#20 statistics-queue-1446932-20.patch12.32 KBtimmillwood
#13 statistics-queue-1446932-13.patch13.44 KBtimmillwood
#9 statistics-queue-1446932-9.patch12.94 KBtimmillwood
#4 statistics_cache_1446932_4.patch1.76 KBtimmillwood
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mikeytown2’s picture

jStats does something similar to your proposal (doesn't use a cache table though)
http://drupal.org/project/jstats

timmillwood’s picture

Right, so jstats writes to one table each page view, then writes to another table on cron.
I think it would be better to use Drupal core cache or queue system then it can hook into another external caching or queuing system such as memcache or redis more easily.

jcisio’s picture

The trick is to replace "N updates" with "N inserts + 1 update" which is known much cheaper in MySQL. It may be not correct/applicable for another backend.

timmillwood’s picture

Status: Active » Needs review
FileSize
1.76 KB

Here's a quick patch for this which creates a cache table for the node_counter and all reads are cached.

TODO: add some way of clearing the cache. Open for offers on when to clear it. Cron run? max-age header? something else?

Status: Needs review » Needs work

The last submitted patch, statistics_cache_1446932_4.patch, failed testing.

jcisio’s picture

@timmillwood I don't see why a

SELECT * FROM node_counter WHERE nid = :nid

is not as good as

SELECT * FROM cache_node_counter WHERE cid = :cid

The first one is even better in some db system, as nid is an integer. The second is only better with another cache backend other than the core default one.

timmillwood’s picture

Status: Needs work » Needs review

The advantage of using another cache backend is a big one.

This is a small step in the performance of the statistics module. More to come.

The test is failing because it's reading from cached data, and the first read from the database is when there is no data in there, which gets cached.

timmillwood’s picture

Status: Needs review » Needs work

Re-thinking this, going to write the node_counter updates to a Drupal Queue, then to the node_counter table on cron. (patch coming soon)

timmillwood’s picture

This patch seems to work, but I am still looking through issues in the test.

I just wanted to get something up here for now to get my general ideas across.

This patch writes node views to a queue, then each cron run these are written to the node_counter table. The real advantage of this comes when using an alternative backend for the queue.

timmillwood’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, statistics-queue-1446932-9.patch, failed testing.

rjbrown99’s picture

I'm starting to play around with a variant of #4 with Drupal 6. It's working well, and my take is to set the cache expiration via a variable, then only cache for the current time + that variable. I set it to 15 minutes or 900 seconds by default. So I'm not clearing at all via cron, just allowing the cache to expire naturally.

I'd move to another stats module, but I have a bunch of stuff tied to core statistics so it's easier for me to just improve it than rethink everything else.

Here is what my D6 version looks like for reference (not that it will ever have a chance of being added.) One note to anyone else that tries to use it, my version of Drupal 6 has a backport of REQUEST_TIME which is not in D6 by default. If you try to use this you need to replace that with time().

I'll probably play with drupal_queue backport next with the other patches.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
13.44 KB

This patch is basically the same as the last just with an updated test so it should pass.

Status: Needs review » Needs work

The last submitted patch, statistics-queue-1446932-13.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review

#13: statistics-queue-1446932-13.patch queued for re-testing.

timmillwood’s picture

Wow! Not sure why but it seemed to fail every related test, worked fine locally!

Status: Needs review » Needs work

The last submitted patch, statistics-queue-1446932-13.patch, failed testing.

timmillwood’s picture

After a local pull the test is now failing, but the patch still works. There must've been a change which is causing the test to fail.

tstoeckler’s picture

In the tests, you should use

$this->cronRun();

instead of calling the URL on your own.
(Btw: cron.php was nuked recently, so that will be the cause of some of the failures.)

timmillwood’s picture

Status: Needs work » Needs review
FileSize
12.32 KB

Don't think the tests are 100% there, replaced calling the cron URL to use $this->cronRun();

Status: Needs review » Needs work

The last submitted patch, statistics-queue-1446932-20.patch, failed testing.

tstoeckler’s picture

Some (mostly) minor clean-up stuff:

+++ b/core/modules/search/search.test
@@ -459,6 +459,8 @@ class SearchRankingTestCase extends SearchWebTestCase {
+    $key = variable_get('cron_key', 'drupal');
+    $this->drupalGet($base_url . '/core/cron.php', array('external' => TRUE, 'query' => array('cron_key' => $key)));

Leftover call to old cron.php

+++ b/core/modules/statistics/statistics.module
@@ -231,6 +231,21 @@ function statistics_user_predelete($account) {
+  ¶

+++ b/core/modules/statistics/statistics.test
@@ -373,38 +378,39 @@ class StatisticsAdminTestCase extends DrupalWebTestCase {
+    ¶
...
+    ¶
...
+    ¶
...
+    ¶

Trailing whitespace.

+++ b/core/modules/statistics/statistics.php
@@ -14,20 +14,12 @@ chdir('../../..');
-drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
+drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

This seems like it could actually be a perfomance hit, even if it's only a small one. Should this be benchmarked for people with the default queueing backend?

+++ b/core/modules/statistics/statistics.test
@@ -96,6 +96,7 @@ class StatisticsLoggingTestCase extends DrupalWebTestCase {
+    global $base_url;

@@ -232,9 +234,9 @@ class StatisticsReportsTestCase extends StatisticsTestCase {
+    global $base_url;

@@ -361,6 +365,7 @@ class StatisticsAdminTestCase extends DrupalWebTestCase {
+    global $base_url;

@@ -373,38 +378,39 @@ class StatisticsAdminTestCase extends DrupalWebTestCase {
+    global $base_url;

@@ -465,21 +473,20 @@ class StatisticsAdminTestCase extends DrupalWebTestCase {
+    global $base_url;

@@ -519,6 +526,7 @@ class StatisticsTokenReplaceTestCase extends StatisticsTestCase {
+    global $base_url;

This is not needed now that we use $this->drupalCron()

timmillwood’s picture

The full bootstrap will be a performance hit, but once dependency injection and other symfony things are sorted out, I will move it over to use that, and lower the bootstrap.

$base_url is needed for statistics.php call.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
13.89 KB

Finally got it to pass the tests locally, fingers crossed all looks ok.

msonnabaum’s picture

Status: Needs review » Needs work

I dont see the point of this. We're just trading one INSERT for another. This assumes that you have a high performance (non-mysql) queue backend and that's why it's a better option. I think a better approach would be to have the stats backend be swappable.

timmillwood’s picture

Title: Improve statistics performance by adding caching or queues » Improve statistics performance by adding a swappable backend
FileSize
4 KB

Here's the first patch for this, but returns the following error as a response from statistics.php

Fatal error: Interface 'Drupal\Core\Statistics\StatisticsBackendInterface' not found in /Users/tim/Sites/statsback/core/lib/Drupal/Core/Statistics/DatabaseStatisticsBackend.php on line 6

timmillwood’s picture

Status: Needs work » Needs review
FileSize
4.28 KB

This one is now working.

RobLoach’s picture

FileSize
3.18 KB

What if we were to move it all into the Statistics module itself? Also don't think we need StatisticsBackend wrapper since you can just re-register "statistics" with a different class if you wanted.

RobLoach’s picture

FileSize
3.18 KB

Had the wrong namespace in the interface.

Status: Needs review » Needs work

The last submitted patch, 1446932.patch, failed testing.

RobLoach’s picture

Status: Needs work » Needs review
FileSize
3.18 KB

Huh?

timmillwood’s picture

Status: Needs review » Needs work

This patch is not working for me. It causes statistics.php to return:
Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The service definition "statistics" does not exist. in Symfony\Component\DependencyInjection\ContainerBuilder->getDefinition() (line 690 of /Users/tim/Sites/statsback/core/vendor/Symfony/Component/DependencyInjection/ContainerBuilder.php).

timmillwood’s picture

I was able to resolve this by clearing cache.

timmillwood’s picture

FileSize
4.09 KB

This patch added variable get to statistics_init to make changing the backend class a bit easier.

It also adds a get method which is uses in statistics_get

timmillwood’s picture

Status: Needs work » Needs review

please review

timmillwood’s picture

FileSize
4.58 KB

Might as well add delete too ;)

Status: Needs review » Needs work

The last submitted patch, 1446932_3.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
4.58 KB

This passes all the tests on my local machine.

timmillwood’s picture

FileSize
5.19 KB

Adding reset method in to set the day counter in node_counter to 0.

Status: Needs review » Needs work

The last submitted patch, 1446932_5.patch, failed testing.

timmillwood’s picture

Not sure why this is failing testing.

timmillwood’s picture

FileSize
5.07 KB
timmillwood’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 1446932_6.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review

#31: 1446932.patch queued for re-testing.

alansaviolobo queued 42: 1446932_6.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 42: 1446932_6.patch, failed testing.

alansaviolobo’s picture

Issue summary: View changes
Issue tags: +Needs reroll
timmillwood’s picture

Version: 8.0.x-dev » 8.1.x-dev
Issue summary: View changes

Bringing this issue back to life, planning to move everything to a service as described in https://www.drupal.org/node/2306083 and https://github.com/palantirnet/hugs/blob/master/src/HugTracker.php

timmillwood’s picture

Status: Needs work » Needs review
FileSize
2.33 KB

First step in regards to making this work

Status: Needs review » Needs work

The last submitted patch, 50: improve_statistics-1446932-50.patch, failed testing.

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 50: improve_statistics-1446932-50.patch, failed testing.

dawehner’s picture

+++ b/core/modules/statistics/src/StatisticsBackend.php
@@ -0,0 +1,38 @@
+class StatisticsBackend {

Some interface could we certainly useful, otherwise, well it would be totally pointless :)

timmillwood’s picture

Status: Needs work » Needs review
FileSize
3.04 KB

Adding an interface to the initial service.

Status: Needs review » Needs work

The last submitted patch, 55: improve_statistics-1446932-55.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
7.24 KB

Adding all database queries from statistics.module to the service, apart from the one in the statistics_title_list function as this is a little more complex because it joins other database tables which other backends wouldn't be able to do.

Status: Needs review » Needs work

The last submitted patch, 57: improve_statistics-1446932-57.patch, failed testing.

rpayanm’s picture

Status: Needs work » Needs review
FileSize
7.24 KB

Rerolling...

Status: Needs review » Needs work

The last submitted patch, 59: 1446932-59.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
0 bytes

Switching statistics_title_list function to call the statisticsTitleList storage service method too, I think this is now all DB queries into the service.

timmillwood’s picture

FileSize
9.92 KB

Oops, empty patch.

Hope this one works!

Status: Needs review » Needs work

The last submitted patch, 62: improve_statistics-1446932-62.patch, failed testing.

The last submitted patch, 61: improve_statistics-1446932-61.patch, failed testing.

timmillwood’s picture

Version: 8.1.x-dev » 8.0.x-dev
Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 62: improve_statistics-1446932-62.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
9.86 KB
timmillwood’s picture

Removing the "statistics_title_list" function and calling the storage service directly from the block plugin.

dawehner’s picture

  1. +++ b/core/modules/statistics/src/StatisticsStorage.php
    @@ -0,0 +1,106 @@
    +class StatisticsStorage implements StatisticsStorageInterface {
    

    What about naming the class to make it clear that its for database ... what about using StatisticsDatabaseStorage

  2. +++ b/core/modules/statistics/src/StatisticsStorage.php
    @@ -0,0 +1,106 @@
    +  if (in_array($dbfield, array('totalcount', 'daycount', 'timestamp'))) {
    ...
    +  }
    +  return FALSE;
    

    Do you mind indenting this :)

  3. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,86 @@
    +/**
    + * Provides an interface defining Statistics Storage
    + */
    

    A little bit of docs about what is stored in the stats would be great

  4. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,86 @@
    +  public function recordHit($nid);
    

    I still don't get why we don't make some more useful interface, given that we have a completely new API anyway.

Removing the "statistics_title_list" function and calling the storage service directly from the block plugin.

This sounds like a unnecessary API removal.

timmillwood’s picture

Status: Needs review » Needs work
Issue tags: -Needs reroll

I still don't get why we don't make some more useful interface, given that we have a completely new API anyway.

What were you thinking? Maybe better as a new issue? Already thinking of making. A new patch to make it entity counter rather than node counter.

Statistics_title_list function made no sense as it was just calling statisticsTitleList method, now just calling the method directly from the block.

All other comments sound fine, will update patch.

Thanks for reviewing!!!!

timmillwood’s picture

Updating re: #70

timmillwood’s picture

Status: Needs work » Needs review

Status: Needs review

Status: Needs review » Needs work

The last submitted patch, 72: improve_statistics-1446932-72.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
12.62 KB

Oops, looks like I missed the new file from the patch when renaming.

dawehner’s picture

What were you thinking? Maybe better as a new issue? Already thinking of making. A new patch to make it entity counter rather than node counter.

Well, I think we should avoid API breakage for things we introduce ....

timmillwood’s picture

So you think we should keep all the function in statistics.module and just call the new service not to break the API?

Crell’s picture

Status: Needs review » Needs work

At this stage in the cycle we probably have to keep the old functions and mark @deprecated. But be aggressive in what we mark @deprecated now that there is a service to use instead.

  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -58,18 +58,20 @@ public function defaultConfiguration() {
    +
    +    $storage = \Drupal::service('statistics.statistics_storage');
    

    This isn't a static method, so inject the service properly. \Drupal:: inside a class is a code smell, always.

  2. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,106 @@
    +    return (bool) $this->connection->merge('node_counter')
    +      ->key('nid', $nid)
    +    ->fields(array(
    +      'daycount' => 1,
    +      'totalcount' => 1,
    +      'timestamp' => REQUEST_TIME,
    +    ))
    +      ->expression('daycount', 'daycount + 1')
    +        ->expression('totalcount', 'totalcount + 1')
    +          ->execute();
    

    The whitespacing here is AFU. :-) Please standardize. (Same in other methods, too. Looks like default indentation from an IDE that wasn't cleaned up.)

  3. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,106 @@
    +    if ($nid > 0) {
    

    Is this really a check that's needed? If so, sounds more like an assertion type check.

  4. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,106 @@
    +      // Retrieve an array with both totalcount and daycount.
    

    Except there's a 3rd value here that's also returned.

  5. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,106 @@
    +    $q = $this->connection->select('node_counter', 'nc');
    +    $q->addExpression('MAX(totalcount)');
    +    return (int) $q->execute()->fetchField();
    

    Conventionally we're using $query, not $q, for query objects. Single-character variable names are almost never useful.

  6. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,88 @@
    +   * Returns if node view is counted
    

    Period at the end of the line. (Same for other docblocks.)

    Also, this description is wrong. recordHit() updates data. The text here suggests its just a reporting tool, which is not at all the case. Don't just duplicate the @return doc unless that's really *all* that happens.

  7. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,88 @@
    +   *   The id of the node to count
    

    Ibid.

  8. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,88 @@
    +   * Returns if day count is reset
    

    As above, this method *modifies data*. The summary line should say why you'd call it, not replicate @return.

  9. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,88 @@
    +   * @return SelectQuery|FALSE
    +   *   A query result containing the node ID, title, user ID that owns the node,
    +   *   and the username for the selected node(s), or FALSE if the query could not
    +   *   be executed correctly.
    

    Don't return a DB object. That's breaking encapsulation. Return just the data you're asking for.

    Looking at it again... is this even a relevant method for the interface? Seems like an internal implementation detail of the DB implementation (and thus should be only a protected method).

  10. +++ b/core/modules/statistics/statistics.module
    @@ -68,61 +68,21 @@ function statistics_node_links_alter(array &$node_links, NodeInterface $entity,
    +  $storage = \Drupal::service('statistics.statistics_storage');
       $statistics_timestamp = \Drupal::state()->get('statistics.day_timestamp') ?: 0;
    

    If the state system is always updated at the same time as the service, should it not be internalized into the service?

    if ($storage->needsReset()) {
    $storage->resetDayCount();
    }

    That needsReset is pulling from the state API under the hood is not this hook's business.

  11. +++ b/core/modules/statistics/statistics.module
    @@ -68,61 +68,21 @@ function statistics_node_links_alter(array &$node_links, NodeInterface $entity,
    +    // Reset day counts
    +    $storage->resetDayCount();
    

    Comment is now redundant.

  12. +++ b/core/modules/statistics/statistics.module
    @@ -68,61 +68,21 @@ function statistics_node_links_alter(array &$node_links, NodeInterface $entity,
       // Calculate the maximum of node views, for node search ranking.
    -  \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, db_query('SELECT MAX(totalcount) FROM {node_counter}')->fetchField()));
    +  $max_total_count = $storage->maxTotalCount();
    +  \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, $max_total_count));
    

    Same here. Let's hide the state API usage behind the service as well. Then ask how much should be shared between possible implementations of that service.

  13. +++ b/core/modules/statistics/statistics.module
    @@ -137,10 +97,9 @@ function statistics_title_list($dbfield, $dbrows) {
     function statistics_get($nid) {
    -
       if ($nid > 0) {
    -    // Retrieve an array with both totalcount and daycount.
    -    return db_query('SELECT totalcount, daycount, timestamp FROM {node_counter} WHERE nid = :nid', array(':nid' => $nid), array('target' => 'replica'))->fetchAssoc();
    +    $storage = \Drupal::service('statistics.statistics_storage');
    +    return $storage->fetchViews($nid);
       }
     }
    

    Seems this function should now be marked @deprecated? It's a one liner now, or should be. (return Drupal::service()->fetchViews());

timmillwood’s picture

Thanks @Crell!

Implemented suggestions from #78.

Crell’s picture

  1. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -14,13 +15,23 @@ class StatisticsDatabaseStorage implements StatisticsStorageInterface {
       /**
    +   * The state service.
    +   *
    +   * @var \Drupal\Core\State\StateInterface
    +   */
    +  protected $state;
    +  ¶
    +  /**
    

    Dreditor says there's a stray whitespace on line 24.

  2. +++ b/core/modules/statistics/statistics.module
    @@ -69,37 +69,30 @@ function statistics_node_links_alter(array &$node_links, NodeInterface $entity,
    + * @deprecated in Drupal 8.0.x
    

    @deprecated markers should indicate what to use instead.

  3. +++ b/core/modules/statistics/statistics.module
    @@ -69,37 +69,30 @@ function statistics_node_links_alter(array &$node_links, NodeInterface $entity,
    +function statistics_title_list($dbfield, $dbrows) {
    +  return \Drupal::service('statistics.statistics_storage')->statisticsTitleList($dbfield, $dbrows);
    +}
    

    This is now calling a method that is not on the interface, which means implicitly coupling it to a single implementation. You don't want that. :-)

    Stepping back, what's the use of this function/method? How can it be described in a non-DB-dependent way?

timmillwood’s picture

3. This is used in the block and the output passed into node_title_list. The query uses more than just the statistics' node_counter table.

Open for suggestions.

timmillwood’s picture

Fixed 1&2

Added statisticsTitleList back to the interface. Developers overriding the interface will need to make sure it returns a database result object for node_title_list. I've added a @todo for me to rework this so that the minimum is needed from the service and a database result object for node_title_list is created elsewhere.

Crell’s picture

#82: Here's the problem. Say you write a MongoDB implementation of the storage class. Now statisticsTitleList returns... what? Having it return an SQL-related object makes no sense at all, and is probably not even feasible. That's not going to fly.

Tim: What is the data that a caller needs to get back from that method, and how would they use it? Specifically, can you describe it in a way that does not involve the word "result" or "database"?

timmillwood’s picture

Huge rework of the Statistics popular block to make it less dependant on the database.

dawehner’s picture

  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +177,37 @@ public function build() {
    +    $nodes = $this->entityManager->getStorage('node')->loadMultiple($counts);
    +
    +    $items = array();
    +    foreach ($counts as $count) {
    +      $items [] = \Drupal::l($nodes[$count]->getTitle(), new Url('entity.node.canonical', ['node' => $count], array()));
    

    Is there a reason to not directly use $node->urlInfo()

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +177,37 @@ public function build() {
    +      '#cache' => ['tags' => Cache::mergeTags(['node_list'],Cache::buildTags('node', $counts))]
    

    Nitpick: Missing space after 'node_list'],

  3. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,111 @@
    +
    +class StatisticsDatabaseStorage implements StatisticsStorageInterface {
    

    Needs some docs, if possible

  4. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,111 @@
    +    return (REQUEST_TIME - $statistics_timestamp) >= 86400;
    ...
    +    $this->state->set('statistics.day_timestamp', REQUEST_TIME);
    

    If you want you can also use $request->server->get('REQUEST_TIME')

timmillwood’s picture

Implemented the suggestions from #85, apart from 4 because $request->server->get('REQUEST_TIME') looked to add a bigger overhead than just using REQUEST_TIME .

Would be awesome to see this in before the next Beta next week.

Status: Needs review » Needs work

The last submitted patch, 86: improve_statistics-1446932-86.patch, failed testing.

timmillwood’s picture

The test was looking for \Drupal::l($node->label(), $node->urlInfo('canonical', ['language' => NULL])) and now we're using node entity objects for the block links this wasn't working. Updated the block links to \Drupal::l($nodes[$count]->getTitle(), $nodes[$count]->urlInfo('canonical') and the test to the same (removing the ['language' => NULL] option.

timmillwood’s picture

Status: Needs work » Needs review
Crell’s picture

Getting super close! :-)

  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,37 @@ public function build() {
    +  protected function nodeTitleList($counts, $title){
    

    Space needed before {

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,37 @@ public function build() {
    +      $items [] = \Drupal::l($nodes[$count]->getTitle(), $nodes[$count]->urlInfo('canonical'));
    

    Drupal::l() should be considered deprecated (even though not officially). The preferred way to pass around links is to use the Url object, which can safely be put into a render array and handles its own cache tag information.

    And even baring that, \Drupal:: calls inside a class are a no-no. :-) Inject the generator service yourself.

  3. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,118 @@
    +    // Retrieve an array with both totalcount, daycount and timestamp.
    

    Nitpick: Both implies 2 values, not 3. Also, Drupal uses an Oxford comma like all civilized people.

  4. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,90 @@
    +  /**
    +   * Returns the number of times a node has been viewed.
    +   *
    +   * @param strign $order
    +   *   The column name to order by.
    +   *
    +   * @return array
    +   *   An ordered array of node ids.
    +   */
    +  public function fetchAll($order = 'totalcount', $limit = 5);
    

    Typo on "strign".

    Also, the "column to order by" is not descriptive. How do I know what the columns are? Column is an SQL-ism.

    Instead, say the counter to order by, then list the valid options and what they mean. (That in the SQL implementation they happen to map to a column in a table is a coincidence.) I'd go a step further and then stick an assert() statement in the implementation to verify that $order is one of those allowed values. :-) (I blame AkiTendo.)

  5. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,90 @@
    +  /**
    +   * Returns if reset is needed.
    +   *
    +   * @return bool
    +   *  TRUE if reset is needed.
    +   */
    +  public function needsReset();
    

    The docblock should explain what reset is and why it would be needed.

timmillwood’s picture

Status: Needs review » Needs work

@Crell - do you have any examples for 2.?

timmillwood’s picture

Status: Needs work » Needs review
FileSize
4 KB
17.71 KB

Implemented everything from #90

Crell’s picture

  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -193,19 +193,31 @@ public function build() {
    -      $items [] = \Drupal::l($nodes[$count]->getTitle(), $nodes[$count]->urlInfo('canonical'));
    +      // $items [] = \Drupal::l($nodes[$count]->getTitle(), $nodes[$count]->urlInfo('canonical'));
    +      $items [] = array(
    +        '#type' => 'link',
    +        '#title' => $nodes[$count]->getTitle(),
    +        '#url' => $nodes[$count]->urlInfo('canonical'),
    +        '#cache' => array(
    +          'context' => $nodes[$count]->getCacheContexts(),
    +          'tags' => $nodes[$count]->getCacheTags(),
    +        ),
    +      );
    

    Forgot to remove the commented out line. :-)

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -193,19 +193,31 @@ public function build() {
    -      '#cache' => ['tags' => Cache::mergeTags(['node_list'], Cache::buildTags('node', $counts))]
    +      '#cache' => array(
    +          'context' => 'user.roles',
    +          'tags' => $this->entityManager->getDefinition('node')->getListCacheTags(),
    +        ),
           );
    

    I've no idea of this is correct or not. :-)

  3. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -71,10 +71,13 @@ public function fetchViews($nid) {
    +    if (in_array($order, array('totalcount', 'daycount', 'timestamp'))) {
    +      return $this->connection->select('node_counter', 'nc')
    +        ->fields('nc', array('nid'))
    +        ->orderBy($order, 'DESC')->range(0, $limit)
    +        ->execute()->fetchCol();
    +    }
    +    return FALSE;
    

    Regarding returning false from a method: http://www.garfieldtech.com/blog/empty-return-values

    If we're allowed to use assertions, that would be ideal. If not, throw \InvalidArgumentException.

  4. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -64,7 +67,10 @@ public function fetchAll($order = 'totalcount', $limit = 5);
       /**
    -   * Returns if reset is needed.
    +   * A reset is performed daily.
    +   * Returns if 24 hours has passed since the last reset.
    +   *
    +   * @see StatisticsStorageInterface::resetDayCount()  To perform the reset.
    

    Needs linebreak between first line and description. Plus the first line should be properly structured. Suggested:

    "Returns true if it's been more than 24 hours since daily totals have been reset."

timmillwood’s picture

Suggestions from #93 (apart from 2. which ideally needs someone like Wim to review).

timmillwood’s picture

@Crell is fussy when it comes to comments ;)

-   * Returns true if it's been more than 24 hours since daily totals have been
-   * reset.
+   * Returns true if more than 24 hours since daily totals have been reset.
Crell’s picture

Hey, it's Drupal's coding standard, not mine. :-) Looks good to me, modulo verifying the cache context info. RTBC aside from that. Nice work, Tim!

Fabianx’s picture

  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,49 @@ public function build() {
     
    +  protected function nodeTitleList($counts, $title) {
    

    Missing doxygen.

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,49 @@ public function build() {
    +      '#cache' => array(
    +          'context' => 'user.roles',
    +          'tags' => $this->entityManager->getDefinition('node')->getListCacheTags(),
    +        ),
    

    Should this be per user.role as in the role is different or is this permissions based?

    In that case should use user.permissions.

    --

    If we add the node list tag here, we could remove the tags from above as that one will override the other ones.

    Do we really need the node list tags here?

    Overall +1 to add cacheable metadata though :).

timmillwood’s picture

Taking on suggestions from #97

Thanks Fabian!

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Aaaaand scene. :-)

timmillwood’s picture

Issue summary: View changes
FileSize
1008.63 KB

Thanks Crell!

Fingers crossed for Wednesday's beta!

Fabianx’s picture

+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -193,6 +193,18 @@ public function build() {
+   * The title for the list. ¶

nit nit nit nit - superfluous space at end of line, can be fixed at commit time.


RTBC + 1

timmillwood’s picture

Assigned: timmillwood » Unassigned

Unassigning from me.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs beta evaluation
  1. This issue is a task so we need to outline how it fits within the allowable Drupal 8 beta criteria. Can someone add Drupal 8 beta phase evaluation template to the issue summary. That said, in my mind I can't see how this issue meets the criteria for inclusion in the beta. It looks like a 8.1 change to me - which makes me sad because of all the work that has gone on here already. The change makes lots of sense and is architecturally sound.
  2. +++ b/core/modules/statistics/statistics.module
    @@ -68,79 +68,22 @@ function statistics_node_links_alter(array &$node_links, NodeInterface $entity,
    + * @deprecated in Drupal 8.0.x, use fetchViews in statistics_storage service.
    

    The format for a deprecation is like this:

     * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0.
     *   Use \Drupal::service('email.validator')->isValid().
    
  3. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,60 @@ public function build() {
    +  /**
    +   * Generates the render array for the block.
    +   *
    +   * @param array $counts
    +   * An ordered array of node ids.
    +   *
    +   * @param string $title
    +   * The title for the list. ¶
    +   *
    +   * @return array
    +   *  A render array for the list.
    +   */
    

    Needs to be indented and have space from end of line removed.

  4. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,123 @@
    +  * Construct the statistics storage.
    

    Constructs

  5. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,123 @@
    +        'timestamp' => REQUEST_TIME,
    ...
    +    return (REQUEST_TIME - $statistics_timestamp) >= 86400;
    ...
    +    $this->state->set('statistics.day_timestamp', REQUEST_TIME);
    

    Inject the request stack to get the request time. That makes this more unit testable. You could a have a protected method to get the request time from the current request.

  6. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,100 @@
    + * Provides an interface defining Statistics Storage
    + * Stores the views per day, total views and timestamp of last view
    

    Missing full stop at end of first line and needs a blank line after the first one.

  7. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,100 @@
    +   * The number of node ids to return.
    

    Needs indenting. Plus IDs not ids.

  8. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,100 @@
    +   *   An ordered array of node ids.
    

    The patch needs a general search for id/ids in documentation and replacing with ID/IDs.

  9. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,100 @@
    +  /**
    +   * Delete counts for a specific node.
    +   *
    +   * @param int $nid
    +   *   The id of the node which views to delete.
    +   *
    +   * @return bool
    +   *   TRUE if the node views have been deleted.
    +   */
    +  public function clean($nid);
    

    Clean is an odd method name for deleting a specific node - how about deleteNode() and accept a node entity instead of just the nid - maybe an alternate storage would want to key by uuid. Hmmm I see all the other methods are accepting an nid. So leave it at that but this method should have a better name.

  10. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,100 @@
    +   * Returns true if more than 24 hours since daily totals have been reset.
    

    The 24 hours sounds like an implementation detail and not something that should be on the interface.

alexpott’s picture

Version: 8.0.x-dev » 8.1.x-dev
Status: Needs work » Postponed

After discussing with @xjm and given #103.1 and the fact I can't see how this meets the criteria I'm going to postpone now. Since actioning the other feedback in #103 is taking resources away from fixing what we have to fix to get D8 out of the door.

I'm sorry @timmillwood I wish we'd done the beta evaluation earlier.

timmillwood’s picture

Version: 8.1.x-dev » 8.0.x-dev
Issue summary: View changes
Issue tags: -Needs beta evaluation

I will be working on the issues in #103 so switching this back to 8.0.x for future testing.

I'd still love to see this in 8.0.x as it's one the biggest improvement in the Statistics module ever.

Added a Beta phase evaluation

Wim Leers’s picture

Found additional code style problems on top of those reported by Alex. And more severe problems, but nothing truly severe :)

  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,60 @@ public function build() {
    +    $nodes = $this->entityManager->getStorage('node')->loadMultiple($counts);
    

    Shouldn't this also be doing getTranslationFromContext(), to ensure you get the right translation?

    Suggests missing test coverage.

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,60 @@ public function build() {
    +    $items = array();
    ...
    +    return array(
    

    We use the short array notation for new code.

  3. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,60 @@ public function build() {
    +      $items [] = array(
    

    Should not be a space between $items and [].

  4. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,60 @@ public function build() {
    +        '#cache' => array(
    +          'context' => $nodes[$count]->getCacheContexts(),
    +          'tags' => $nodes[$count]->getCacheTags(),
    +        ),
    

    This looks correct; but I'd still like to see at least one integration test for it.

    You can use assertCacheTags() and assertCacheContexts(); they make it easy to check this :)

    (This is what you pinged me about for a review on Twitter — sorry for the delay!)

  5. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -124,21 +176,60 @@ public function build() {
    +      '#cache' => array(
    +          'context' => 'user.permissions',
    +          'tags' => $this->entityManager->getDefinition('node')->getListCacheTags(),
    +        ),
    +      );
    

    Strange indentation here.

timmillwood’s picture

Status: Postponed » Needs review
FileSize
26.44 KB
15.18 KB

won't fix:
#103.9 - The aim of this function is to remove a node's view count if the node is deleted, so clean seemed right. I was also trying to steer away from using CRUD terms and node (because planning to make this work with all entities as soon as this patch is committed).

#103.10 - The reset is for the daily view count, so reseting at any other timeframe than 24 hrs would be odd.

Todo:
#103.5
#106.4

Wim Leers’s picture

Issue tags: +Performance, +D8 cacheability

(because planning to make this work with all entities as soon as this patch is committed)

View statistics for all entity types? :)

timmillwood’s picture

View statistics for all entity types? :)

Yes! awesome, right?!
(might need to add an option to choose which entity types you want counted and which not)

#2532334: Count all content entities views in the Statistics module

timmillwood’s picture

Adding #103.5 and #106.4 to the patch in #107.

hussainweb’s picture

Fixing a trailing whitespace. I also wanted to see if we could reconsider adding this in 8.0.x. It will make migration much more simpler. I marked #2500521: Upgrade path for Statistics 7.x and 6.x as postponed on this issue. @alexpott, thoughts? I will work on a beta evaluation if you think it makes sense.

hussainweb’s picture

Issue summary: View changes

I noticed that there was already a beta evaluation and updated it. I am going out on a limb and marking this as unfrozen as it helps migration (which is unfrozen). If that is not correct, please feel free to remove the unfrozen clause.

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for the support @hussainweb, I too would love to see this in 8.0.x, got loads more planned for Statistics in 8.1.x

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -121,24 +173,64 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +   * Generates the render array for the block.
    

    This comment is wrong, because it doesn't generate the render array for the entire block, it is merely a helper method for build(), which is the method that would fit this description.

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -121,24 +173,64 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +   * @param array $counts
    +   *   An ordered array of node ids.
    +   *
    +   * @param string $title
    +   *   The title for the list.
    

    The \n between these shouldn't be there.

  3. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -121,24 +173,64 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +  protected function nodeTitleList($counts, $title) {
    ...
    +    foreach ($counts as $count) {
    

    $counts feels like a strange variable name here.

  4. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -121,24 +173,64 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +        'context' => 'user.permissions',
    

    First off, it's 'contexts', and it needs to be an array.

    Second: where does the permission checking happen? I.e. I don't think we actually want this cache context? Perhaps node access grants are being checked somehow (I don't see where though?), and that's why? We have the user.node_grants:view cache contexts for that though. See https://www.drupal.org/developing/api/8/cache/contexts.

  5. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,142 @@
    + * Provides the default database storage backend for statistics module.
    

    Nit: s/module//

  6. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,142 @@
    +  /**
    +  * Constructs the statistics storage.
    +  *
    +  * @param \Drupal\Core\Database\Connection $connection
    +  *   The database connection for the node view storage.
    +  * @param \Drupal\Core\State\StateInterface $state
    +  *   The state service.
    +  */
    ...
    +  /**
    +  * {@inheritdoc}
    +  */
    ...
    +  /**
    +  * {@inheritdoc}
    +  */
    

    Indentation errors in these and other docblocks.

  7. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,142 @@
    +      ->condition('nid', $nid, '=')->execute()->fetchAssoc();
    ...
    +      ->orderBy($order, 'DESC')->range(0, $limit)
    +      ->execute()->fetchCol();
    

    Don't method chain calls belong on new lines?

  8. +++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
    @@ -49,9 +54,16 @@ function testPopularContentBlock() {
    +    $this->assertCacheTags(array_merge(
    

    Please use Cache::mergeTags() instead of array_merge().

timmillwood’s picture

Updating with suggestions from #14, apart from #14.8 because Cache::mergeTags() only accepts 2 params, with array_merge() I can pass 4 in.

Wim Leers’s picture

+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -194,29 +194,28 @@ public function build() {
    * @param array $counts

This is not yet updated.

RE: #14.8: In other tests, we then just call Cache::mergeTags() several times. Yes, annoying, yes, ugly, but that's because we chose to micro-optimize the performance of Cache::mergeTags() for the most common case.

alexpott’s picture

Unfrozen because it helps with migration for node counters.

How exactly does it do this?

hussainweb’s picture

@alexpott: Right now, there is no clean reusable way to write to node_counter table. The statistics module just directly runs queries or calls db_merge() to write. I started with doing the same in the migration destination handler and then realized that a storage service would be better. I then found this issue which does the same thing. See #2500521-4: Upgrade path for Statistics 7.x and 6.x for my comment there and the partial patch I have uploaded.

timmillwood’s picture

timmillwood’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll

needs reroll because of #2543554: Clean up Help & Statistics blocks

I'll take a look tomorrow (GMT), unless anyone else takes it before then.

hussainweb’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
28.16 KB

Rerolled.

alexpott’s picture

@hussainweb well we have a working d6 migration without this.

timmillwood’s picture

Status: Needs review » Needs work
+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -20,17 +25,88 @@
+<<<<<<< HEAD
...
+>>>>>>> statistics

Looks like these are left over from the merge.

Wim Leers’s picture

As promised in #2543554: Clean up Help & Statistics blocks — which broke this — helping to resolve conflicts.

(No credit please.)

hussainweb’s picture

@alexpott: The D6 migrations do not migrate the statistics themselves (which are stored in node_counter table).

I did a test migration from D6 to D8 just now and confirmed that the settings for Statistics module is migrated (which is d6_statistics_settings.yml), but not the data inside node_counter table itself. In fact, I did a grep on whole migrate code base and didn't find anything that would migrate data in node_counter table.

timmillwood’s picture

@hussainweb - Because recordHit($nid) only records one count for the node id I'm not sure it'll help the migration, however we could add to the service to make it work better for migrations.

phenaproxima’s picture

Issue tags: +blocker

This blocks the migration path for Statistics 7.x to 8.x: #2500521: Upgrade path for Statistics 7.x and 6.x

Status: Needs review » Needs work

The last submitted patch, 124: improve_statistics-1446932-124.patch, failed testing.

timmillwood’s picture

The issue looks to be related to the caching tests.

timmillwood’s picture

Version: 8.0.x-dev » 8.1.x-dev
Assigned: Unassigned » timmillwood
Status: Needs work » Needs review
timmillwood’s picture

Status: Needs review » Needs work
Issue tags: -blocker +Needs reroll

Patch doesn't apply anymore ;(

kostyashupenko’s picture

Status: Needs review » Needs work

The last submitted patch, 134: improve_statistics-1446932-134.patch, failed testing.

timmillwood’s picture

@kostyashupenko++

Thanks so much for re-rolling this.

So what, it doesn't pass the tests, at least it now applies. This has been on my todo list for over 6 months!

I will try to find the time over the next week to get this passing so we can get it into 8.1.x

Wim Leers’s picture

Here's a quick review for you. Mostly nits. Because mostly looking good already :)

  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -20,17 +25,64 @@
    +  protected $entityManager;
    

    Since https://www.drupal.org/node/2549139, we shouldn't inject the entity manager anymore.

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -84,31 +136,69 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +   * @param array $nids
    

    So is this int[]?

  3. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -84,31 +136,69 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +  protected function nodeTitleList($nids, $title) {
    

    We can typehint: array $nids

  4. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -84,31 +136,69 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +        '#cache' => [
    +          'contexts' => $node->getCacheContexts(),
    +          'tags' => $node->getCacheTags(),
    +        ],
    +      ];
    

    Better yet would be to use \Drupal\Core\Render\RendererInterface::addCacheableDependency(), then any entities that have max-age set will also have that work, and less manual code here.

  5. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,101 @@
    + * Stores the views per day, total views and timestamp of last view
    + * for all nodes on the site.
    

    80 cols.

  6. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,101 @@
    +  /**
    +   * Returns the number of times a node has been viewed.
    +   *
    +   * @param int $nid
    +   *   The ID of the node to fetch the views for.
    +   *
    +   * @return array
    +   *   An associative array containing:
    +   *   - totalcount: Integer for the total number of times the node has been
    +   *     viewed.
    +   *   - daycount: Integer for the total number of times the node has been viewed
    +   *     "today". For the daycount to be reset, cron must be enabled.
    +   *   - timestamp: Integer for the timestamp of when the node was last viewed.
    +   */
    +  public function fetchViews($nid);
    +
    +  /**
    +   * Returns the number of times a node has been viewed.
    +   *
    +   * @param string $order
    +   *   The counter name to order by:
    +   *   - 'totalcount' The total number of views.
    +   *   - 'daycount' The number of views today.
    +   *   - 'timestamp' The unix timestamp of the last view.
    +   *
    +   * @param int $limit
    +   *   The number of node IDs to return.
    +   *
    +   * @return array
    +   *   An ordered array of node IDs.
    +   */
    +  public function fetchAll($order = 'totalcount', $limit = 5);
    

    Is it allowed to return other things besides totalcount, daycount and timestamp?

    If not, then shouldn't we introduce a value object rather than returning such arrays? That'd enforce alternative implementations to not return additional things.

  7. +++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
    @@ -49,9 +54,22 @@ function testPopularContentBlock() {
    +    $this->assertCacheTags(
    +      Cache::mergeTags(
    +        Cache::mergeTags(
    +          Cache::mergeTags(
    +            $node->getCacheTags(),
    +            $block->getCacheTags()
    +          ),
    +          $this->blockingUser->getCacheTags()
    +        ),
    +        ['block_view', 'config:block_list', 'node_list', 'rendered', 'user_view']
    +      )
    +    );
    

    Eh… wow :P

    What about:

    $tags = Cache::mergeTags($node->getCacheTags(), $block->getCacheTags());
    $tags = Cache::mergeTags($tags, $this->blockingUser->getCacheTags());
    $tags = Cache::mergeTags($tags, ['block_view', 'config:block_list', 'node_list', 'rendered', 'user_view']);
    $this->assertCacheTags($tags);
    
  8. +++ b/core/modules/statistics/statistics.services.yml
    @@ -0,0 +1,6 @@
    +  statistics.statistics_storage:
    +    class: Drupal\statistics\StatisticsDatabaseStorage
    

    Why not just statistics.storage?

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

timmillwood’s picture

Fixed many of the suggestions in #137 but also wanted to retest against 8.2.x

Status: Needs review » Needs work

The last submitted patch, 139: improve_statistics-1446932-139.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
24.95 KB

Fixing tests.

Wim Leers’s picture

Status: Needs review » Needs work

#139 addressed everything in #137 except points 4 and 6.

#141: interdiff is missing.

mparker17’s picture

FileSize
5.44 KB

Here's an interdiff between 139 and 141.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
8.38 KB
27.03 KB

Implementing points 4 & 6 from #137.

andypost’s picture

+++ b/core/modules/statistics/src/StatisticsViews.php
@@ -0,0 +1,48 @@
+}
\ No newline at end of file

nit

timmillwood’s picture

@andypost - Thanks, sure that can be fixed on commit ;)
Although I'll provide a new patch ASAP.

dawehner’s picture

Its still kinda weird for me to introduce an interface which is not entity generic. Could we not provide this interface, but throw an exception for other entity types at least for now?

timmillwood’s picture

Status: Needs review » Needs work

@dawehner - It's always been my plan to make this thing entity generic, but baby steps, nodes first, then open it up. Although I think you make a good point about making at least the interface generic now.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
13.9 KB
36.07 KB

I have addressed the item from #145.

I've also updated many elements such as replacing "node" with "entity", and "nid" with "id", but I think we are a little limited in how generic we can make this in the scope of this issue.

I would like to propose a follow up issue to replace the node_counter table with a statistics table, and make this fully generic. Then on each page load find a way to know which entities have been rendered and count the view of those.

timmillwood’s picture

Assigned: timmillwood » Unassigned

Status: Needs review » Needs work

The last submitted patch, 149: improve_statistics-1446932-149.patch, failed testing.

The last submitted patch, 149: improve_statistics-1446932-149.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
635 bytes
33.04 KB

Fixing broken test.

Manuel Garcia’s picture

+++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
@@ -0,0 +1,150 @@
+    // @todo replace exception with assert() - #2408013.

Can we do this now since #2408013: Adding Assertions to Drupal - Test Tools. is now fixed?

dawehner’s picture

+++ b/core/modules/statistics/statistics.module
@@ -70,79 +70,28 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array
+  $storage = \Drupal::service('statistics.storage');
+  if ($storage->needsReset()) {
+    $storage->resetDayCount();
   }
-
-  // Calculate the maximum of node views, for node search ranking.
-  \Drupal::state()->set('statistics.node_counter_scale', 1.0 / max(1.0, db_query('SELECT MAX(totalcount) FROM {node_counter}')->fetchField()));
+  $storage->maxTotalCount();
 }

What about getting rid of the needsreset method and handle that interally inside $storage? So what just having a reset() method which does both?

timmillwood’s picture

This should address items from #154 and #155.

dawehner’s picture

+++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
@@ -84,9 +84,8 @@ public function fetchViews($id) {
-    // @todo replace exception with assert() - #2408013.
     if (!in_array($order, ['totalcount', 'daycount', 'timestamp'])) {
-      throw new \InvalidArgumentException();
+      assert(false, "Invalid order argument.");
     }

I think what you would use is something like assert('in_array(\'total_count\'...')
This allows the code to not run during runtime then.

timmillwood’s picture

timmillwood’s picture

As this issue in now approaching 4.5 years old it'd be great to get further reviews / RTBC.

dawehner’s picture

  1. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -79,31 +151,67 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +    $nodes = Node::loadMultiple($nids);
    

    You could load the entities by getting the entity storage directly.

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -79,31 +151,67 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    +      $this->renderer->addCacheableDependency($item, $node);
    

    You could use CacheableMetadata::createFromObject($node)->applyTo($item); and get rid of the rendered

  3. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,143 @@
    +    $this->state->set('statistics.node_counter_scale', 1.0 / max(1.0, $max_total_count));
    
    +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,83 @@
    +  /**
    +   * Returns the highest 'totalcount' value.
    +   *
    +   * @return int
    +   *   The highest 'totalcount' value.
    +   */
    +  public function maxTotalCount();
    

    Just from the interface it is not obvious that this also changes state, but I guess this is fine? Still, its own DB update per get query. If we would separate it, we would potentially have less update queries.

  4. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,83 @@
    +/**
    + * @file
    + * Contains \Drupal\statistics\StatisticsStorageInterface.
    + */
    +
    +namespace Drupal\statistics;
    

    nitpick: this isn't needed anymore.

  5. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,83 @@
    + */
    +interface StatisticsStorageInterface {
    

    I love how this interface is not aware of the entity type!

  6. +++ b/core/modules/statistics/src/StatisticsViews.php
    @@ -0,0 +1,48 @@
    +
    +class StatisticsViews implements StatisticsViewsInterface {
    ...
    +  /**
    +   * @var int
    +   */
    +  protected $totalCount;
    +
    +  /**
    +   * @var int
    +   */
    +  protected $dayCount;
    +
    +  /**
    +   * @var int
    +   */
    +  protected $timestamp;
    
    +++ b/core/modules/statistics/src/StatisticsViewsInterface.php
    @@ -0,0 +1,28 @@
    +interface StatisticsViewsInterface {
    

    Some documentation what this object is could be helpful. I'm wondering whether we can come up with a slightly better name, maybe like StatisticsViewsResult, similar to AccessResult?

  7. +++ b/core/modules/statistics/src/Tests/StatisticsReportsTest.php
    @@ -21,7 +26,7 @@ function testPopularContentBlock() {
    -    $post = http_build_query(array('nid' => $nid));
    +    $post = http_build_query(array('id' => $nid));
    ...
         $stats_path = $base_url . '/' . drupal_get_path('module', 'statistics') . '/statistics.php';
    

    Is it okay to have this kind of "api change" on statistics.php? People might still pass along the nid

  8. +++ b/core/modules/statistics/statistics.module
    @@ -52,14 +52,14 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array
    +      $statistics = \Drupal::service('statistics.storage')->fetchViews($entity->id());
    +      if ($statistics instanceof \Drupal\statistics\StatisticsViewsInterface) {
    

    How would it not be instanceof the interface? Isn't it guaranteed to do so?

  9. +++ b/core/modules/statistics/statistics.module
    @@ -161,15 +107,15 @@ function statistics_node_predelete(EntityInterface $node) {
     function statistics_ranking() {
       if (\Drupal::config('statistics.settings')->get('count_content_views')) {
    -    return array(
    -      'views' => array(
    +    return [
    +      'views' => [
             'title' => t('Number of views'),
    -        'join' => array(
    +        'join' => [
               'type' => 'LEFT',
               'table' => 'node_counter',
               'alias' => 'node_counter',
               'on' => 'node_counter.nid = i.sid',
    -        ),
    +        ],
             // Inverse law that maps the highest view count on the site to 1 and 0
             // to 0. Note that the ROUND here is necessary for PostgreSQL and SQLite
             // in order to ensure that the :statistics_scale argument is treated as
    @@ -177,9 +123,9 @@ function statistics_ranking() {
    
    @@ -177,9 +123,9 @@ function statistics_ranking() {
             // values in as strings instead of numbers in complex expressions like
             // this.
             'score' => '2.0 - 2.0 / (1.0 + node_counter.totalcount * (ROUND(:statistics_scale, 4)))',
    -        'arguments' => array(':statistics_scale' => \Drupal::state()->get('statistics.node_counter_scale') ?: 0),
    -      ),
    -    );
    +        'arguments' => [':statistics_scale' => \Drupal::state()->get('statistics.node_counter_scale') ?: 0],
    +      ],
    +    ];
       }
     }
     
    

    Note: the patch contains a lot of unnecessary changes like this one. You know, it just makes it a bit harder to review :)

  10. +++ b/core/modules/statistics/statistics.php
    @@ -14,24 +14,17 @@
     $kernel = DrupalKernel::createFromRequest(Request::createFromGlobals(), $autoloader, 'prod');
    ...
    +    $container->get('request_stack')->push(Request::createFromGlobals());
    

    Note: We create two request objects here, is this really what we want? The second one should not be needed actually

  11. +++ b/core/modules/statistics/statistics.php
    @@ -14,24 +14,17 @@
    -  $nid = filter_input(INPUT_POST, 'nid', FILTER_VALIDATE_INT);
    ...
    +  $id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
    

    Should we have some fallback built in?

Wim Leers’s picture

#160.2: disagreed, this is the best practice. It'd make sense maybe if it's in a critical path, but this is only called when rendering anyway, so the renderer service will definitely already be initialized.

Berdir’s picture

Status: Needs review » Needs work

Reviewed separately from @dawehner, looks like we found many similar things.

  1. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,143 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function fetchViews($id) {
    +    $views =  $this->connection
    +      ->select('node_counter', 'nc')
    +      ->fields('nc', ['totalcount', 'daycount', 'timestamp'])
    +      ->condition('nid', $id, '=')
    +      ->execute()
    +      ->fetchAssoc();
    +    return new StatisticsViews($views['totalcount'], $views['daycount'], $views['timestamp']);
    +  }
    

    Would it make sense to make this multiple or have a multiple version of this?

    Especially for the views field, this could be useful if you don't have do 50 queries on a list with 50 nodes.

    also, static caching might make sense, again to avoid multiple queries when showing day and totalcount in the same view.

  2. +++ b/core/modules/statistics/src/StatisticsDatabaseStorage.php
    @@ -0,0 +1,143 @@
    +   * {@inheritdoc}
    +   */
    +  public function maxTotalCount() {
    +    $query = $this->connection->select('node_counter', 'nc');
    +    $query->addExpression('MAX(totalcount)');
    +    $max_total_count = (int)$query->execute()->fetchField();
    +    $this->state->set('statistics.node_counter_scale', 1.0 / max(1.0, $max_total_count));
    +    return $max_total_count;
    +  }
    

    I don't feel like setting the state belongs into this method, it's just an API, it should assume for what it is used (some search specific scale)

  3. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,83 @@
    +
    +/**
    + * @file
    + * Contains \Drupal\statistics\StatisticsStorageInterface.
    + */
    +
    

    no longer needed.

  4. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,83 @@
    +  /**
    +   * Count a entity view.
    +   *
    +   * @param int $id
    +   *   The ID of the entity to count.
    +   *
    +   * @return bool
    +   *   TRUE if the entity view has been counted.
    +   */
    +  public function recordHit($id);
    

    This is all node specific, talking about entity here seems strange to me.

  5. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,83 @@
    +   *
    +   * @return bool
    +   *   TRUE if the entity views have been deleted.
    +   */
    

    does this really need a return value?

  6. +++ b/core/modules/statistics/src/StatisticsStorageInterface.php
    @@ -0,0 +1,83 @@
    +  /**
    +   * Resets the day count for all entities.
    +   *
    +   * @return bool
    +   *  TRUE if the day count is reset.
    +   */
    +  public function resetDayCount();
    

    I think this needs more description for what is expected from the implementation. you can't just always delete the data, it is your responsibility to check and only do it once per day. (or do nothing, if your backend does it automatically)

  7. +++ b/core/modules/statistics/src/StatisticsViewsInterface.php
    @@ -0,0 +1,28 @@
    +
    +namespace Drupal\statistics;
    +
    +interface StatisticsViewsInterface {
    +
    

    I think we no longer use interfaces for value objects?

  8. +++ b/core/modules/statistics/src/Tests/StatisticsAdminTest.php
    @@ -73,7 +73,7 @@ function testStatisticsSettings() {
         $nid = $this->testNode->id();
    -    $post = array('nid' => $nid);
    +    $post = array('id' => $nid);
         global $base_url;
    

    Isn't this an API change? Doesn't seem necessary or related?

  9. +++ b/core/modules/statistics/statistics.module
    @@ -52,14 +52,14 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array
    +      $statistics = \Drupal::service('statistics.storage')->fetchViews($entity->id());
    +      if ($statistics instanceof \Drupal\statistics\StatisticsViewsInterface) {
    +        $statistics_links['statistics_counter']['title'] = \Drupal::translation()->formatPlural($statistics->getTotalCount(), '1 view', '@count views');
    

    shouldn't use a fully qualified namespace here.

  10. +++ b/core/modules/statistics/statistics.module
    @@ -70,79 +70,26 @@ function statistics_node_links_alter(array &$links, NodeInterface $entity, array
    -function statistics_title_list($dbfield, $dbrows) {
    

    I don't think we can just remove a function like that?

  11. +++ b/core/modules/statistics/statistics.module
    @@ -151,9 +98,8 @@ function statistics_get($nid) {
    +  $id = $node->id();
    +  return \Drupal::service('statistics.storage')->clean($id);
    

    clean() doesn't seem very self-explaining to me, sounds more like a general purpose garbage collection method that we have elsewhere.

    We have fetchViews() (and recordHit, why not view?), so why not deleteViews($id)?

  12. +++ b/core/modules/statistics/statistics.module
    @@ -161,15 +107,15 @@ function statistics_node_predelete(EntityInterface $node) {
     function statistics_ranking() {
       if (\Drupal::config('statistics.settings')->get('count_content_views')) {
    -    return array(
    -      'views' => array(
    +    return [
    +      'views' => [
    

    all those changes look unrelated?

  13. +++ b/core/modules/statistics/statistics.php
    @@ -14,24 +14,17 @@
    +  $id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
    +  if ($id) {
    +    $container->get('request_stack')->push(Request::createFromGlobals());
    +    $container->get('statistics.storage')->recordHit($id);
       }
    

    this is a bit of a regression performance wise... bunch of additional services

Berdir’s picture

+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -79,31 +151,67 @@ public function blockSubmit($form, FormStateInterface $form_state) {
+      '#title' => $title,
+      '#cache' => [
+        'tags' => $this->entityTypeManager->getDefinition('node')->getListCacheTags(),

We have a specific list of nodes now in this method, so we no longer need the list cache tag *here*. We do however still need it in the block in general.

mallezie’s picture

Priority: Normal » Major
Status: Needs work » Needs review
FileSize
26.89 KB
15.44 KB

Patch still applies, so starting to work on feedback.

Comment #160

  1. Done
  2. Not done, see comment #161
  3. Done. Removed the state setting from the method.
  4. Done
  5. Nothing to do
  6. Renamed to StatisticsViewsResult. Added some minor docs.
  7. No idea here.
  8. True, changed, check was not there in statistics_get, so consistency here.
  9. Undone those and other array() => [] conversions, we can do those in 8.2 beta.
  10. Done
  11. See 7 as well.

Comment #162

  1. Could we do this in a follow-up?
  2. See 3 from #160
  3. See 4 from #160
  4. Let's keep it at entity here, we wan't to expend to entities in, and in theory the interface can handle it #2532334: Count all content entities views in the Statistics module
  5. Removed
  6. Added some minor comment tweak
  7. Removed the interface
  8. No idea about the API change
  9. Code is removed, unneeded check.
  10. Readded
  11. Renamed to RecordView (singular on purposes) and deleteViews
  12. Removed array() => [] conversions
  13. One of them is removed, so performance wise probably a bit better, should we discard the service here, and hardcode the count call?

#163

Not sure what to do here. Any pointers are welcome.

This last one, and the $nid -> $id change which could be a BC break are only two not fixed here according to me. Let's hope to finish this one before 8.2 closes. If i read this correctly we can change a lot of things afterwards when we get this one in. Tentatively moving to major therefore.

Status: Needs review » Needs work

The last submitted patch, 164: improve_statistics-1446932-164.patch, failed testing.

mallezie’s picture

Status: Needs work » Needs review
FileSize
26.89 KB
597 bytes

Fix some tests.

timmillwood’s picture

I guess for #163 we just remove the cache tag?

Also $nid -> $id, I think it's safe to keep using $nid for now.

Status: Needs review » Needs work

The last submitted patch, 166: improve_statistics-1446932-166.patch, failed testing.

mallezie’s picture

Status: Needs work » Needs review
FileSize
20.5 KB
7.93 KB

Thanks cache tag and nid->id changes removed.
And a hunch for the last test fails. Not sure however, let's see.

Status: Needs review » Needs work

The last submitted patch, 169: improve_statistics-1446932-169.patch, failed testing.

mallezie’s picture

Status: Needs work » Needs review
FileSize
20.5 KB
556 bytes

And missed on nid -> id conversion.

This should pass. However the performance concern from Berdir (#162 point 13 is still open here). Not sure how much this is an issue?

Status: Needs review » Needs work

The last submitted patch, 171: improve_statistics-1446932-171.patch, failed testing.

mallezie’s picture

Status: Needs work » Needs review
FileSize
20.67 KB
1.82 KB

Last one for today.
Fixes another test, and merges some docs. I can't figure out where those last fails are coming from, if someone could help here, that would be highly appreciated.

Status: Needs review » Needs work

The last submitted patch, 173: improve_statistics-1446932-173.patch, failed testing.

timmillwood’s picture

@mallezie - I had a quick look and it seems as though for StatisticsAdminTest the node_counter table isn't getting setup, I see \Drupal\statistics\StatisticsDatabaseStorage::recordView throwing an exception.

No idea why, there doesn't seem to be any notable difference between this test and the other test.

Berdir’s picture

Re #164:

1. Doing it later is an API addition and will be more complicated than just doing it now. But I don't have a strong opinion, it's not a regression to now I think.
4. I don't see how the interface can handle it with a single argument? Either you talk about node and just have an ID, or you support all entities (maybe throw an exception on anything but node) and have two arguments. You can't change it later.

Sorry, #163 was confusing. This is a bit tricky. Technically, you do need the list cache tag, just like node_title_list. So the code there was pretty much correct. merging in the id cache tags would be "more correct". Problem is, that means *every* time a node is created or saved or deleted, this block needs to be rebuilt. That's quite slow. Especially since we care more about changes in hits than new or updated nodes.

So what I think would be the best combination is only add the cache tags for the specific nodes and set the same max age as we do in statistics_node_links_alter(). That would have the following effect:
* The block is refreshed once per hour, reflecting hit changes
* The block is refreshed when the nodes that are currently shown are updated (so we get a title change for example) or deleted
* The block is *not* updated when new nodes are created or nodes other than the ones shown are updated.

However, this is not really related to this issue, so I'm fine with keeping the current behavior (list cache tag + ids) and deal with max age in a follow-up. (We definitely should create that follow-up though).

mallezie’s picture

Status: Needs work » Needs review
FileSize
20.8 KB
6.06 KB

Thanks for the reviews.

this patch fixes the test failures, and adds a fetch multiple views method. (Caching could be done in a followup if i'm not mistaken). And reverted the node_list caching.

Todo:
-Discuss the entity type genericness, since this is not done here. Thanks berdir fro pointing that out, this indeed needs to be done.
-Create followup for caching without node_list things
-Create followup for Static caching for fetch view method

I'll get on those next. (Just testing first).

mallezie’s picture

Status: Needs review » Needs work

Yes. Green!

mallezie’s picture

Could we rename StatisticsDatabaseStorage to NodeStatisticsDatabaseStorage? And get away for the entity agnostic approach that way?

I like the fact that the interface is not bound to an entity type. We can then just create a new backend for new (all entities for example) statistics and implement with an entity_count table for example.

mallezie’s picture

Status: Needs work » Needs review
FileSize
20.82 KB
8.65 KB

Renamed to NodeStatisticsDatabaseStorage.

Created cache improvement issue: #2776223: Adjust caching for StatisticsPopularBlock
Created static caching issue: #2776225: Use static caching for fetching View

mallezie’s picture

Left a debug statement in.

Berdir’s picture

Could we rename StatisticsDatabaseStorage to NodeStatisticsDatabaseStorage? And get away for the entity agnostic approach that way?

I like the fact that the interface is not bound to an entity type. We can then just create a new backend for new (all entities for example) statistics and implement with an entity_count table for example.

I don't really get how exactly you plan to do this. The interface is still not using an entity type, which means using it for a generic implementation would be impossible. It's not just the implementation, also the interface that is node specific. If you want to rename that too, that would work. But then we have to be clear that there is no easy way to support other entity types.

The alternative approach would be to add the entity type argument to all relevant methods in the interface and throw an exception if it's not node. That would be much easier to replace with a more generic implementation. Also possibly by a contrib module instead of core.

I'm not saying that either option is better, but we have to decide between the two, what you have right now is kind of like a middle thing that I don't see how it would work exactly :)

dawehner’s picture

I don't really get how exactly you plan to do this. The interface is still not using an entity type, which means using it for a generic implementation would be impossible. It's not just the implementation, also the interface that is node specific. If you want to rename that too, that would work. But then we have to be clear that there is no easy way to support other entity types.

Well, you could have in the future a factory in the container which returns the instance you actually want to use. For now it would be great to have 'node' somehow as part of the service name, to indicate what this is actually about.

mallezie’s picture

So if i read this correctly we shoul rename the interface StatisticsStorageInterface to NodeStatisticsStorageInterface? (And adjust the entity specific comments?)

timmillwood’s picture

I wonder if we can update the interface to be generic, accepting either entity_type_id, and entity_id, or just entity. The service could stay node specific for now, but just use the generic interface?

Berdir’s picture

So if i read this correctly we shoul rename the interface StatisticsStorageInterface to NodeStatisticsStorageInterface? (And adjust the entity specific comments?)

I wonder if we can update the interface to be generic, accepting either entity_type_id, and entity_id, or just entity. The service could stay node specific for now, but just use the generic interface?

Yes, one of those two options is what I was suggesting :)

But @dawehner has a point, that would work as well. Generic interface, node specific implementation (current patch), then we should rename the current service to statistics.storage.node. And later, we could add a factory as statistics.storage_factory or so, where you could do this: \Drupal::service('statistics.storage_factory')->getStorage('comment')->... and the generic implementation would get the entity type injected in the constructor.

So those are our options:
1) Node specific interface and implemention. later: both a new interface and implementation.
2) generic Interface with $entity_type_id and $id methods, currently only a node specific implementation, throw exception for other types. later: provide a generic implementation.
3) node specific implementation, generic interface without entity type argument. later: a factory that creates storages for an entity type.

And I guess we should rename the service to storage.node no matter what option we pick.

I like 1) the least, as it doesn't really offer a way forward. both 2 and 3 sounds fine to me. So now we have to decide ;)

  1. +++ b/core/modules/statistics/statistics.module
    @@ -123,26 +115,21 @@ function statistics_title_list($dbfield, $dbrows) {
    -    // Retrieve an array with both totalcount and daycount.
    -    return db_query('SELECT totalcount, daycount, timestamp FROM {node_counter} WHERE nid = :nid', array(':nid' => $nid), array('target' => 'replica'))->fetchAssoc();
    +function statistics_get($id) {
    +  if ($id > 0) {
    +    /** @var \Drupal\statistics\StatisticsViewsResult $statistics */
    +    $statistics = \Drupal::service('statistics.storage')->fetchView($id);
    +    return [
    +      'totalcount' => $statistics->getTotalCount(),
    +      'daycount' => $statistics->getDayCount(),
    +      'timestamp' => $statistics->getTimestamp(),
    +    ];
    

    hm, one feature we seem to lose here is the ability to read this data from the replicate db server if configured. To keep this, we'd have to inject two different connections, which has the downside that it would open the connection to the replica already. not sure if it's worth it.

  2. +++ b/core/modules/statistics/statistics.php
    @@ -14,24 +14,17 @@
    +  $id = filter_input(INPUT_POST, 'nid', FILTER_VALIDATE_INT);
    +  if ($id) {
    +    $container->get('request_stack')->push(Request::createFromGlobals());
    +    $container->get('statistics.storage')->recordView($id);
       }
    

    nitpick: patch gets even a bit smaller if we do not change $nid to $id here.

timmillwood’s picture

1) yes, not sure it's worth it either.
2) Good point!

mallezie’s picture

I really like dawehners option the most. So option 3.
- Renamed the service here
- Made the patch smaller.

I think this is approaching readyness.

Status: Needs review » Needs work

The last submitted patch, 188: improve_statistics-1446932-188.patch, failed testing.

mallezie’s picture

Status: Needs work » Needs review

Did pass ;-)

timmillwood’s picture

I guess the final thing before RTBC is making the interface entity type generic.

Berdir’s picture

No, we don't have to do that with 3. The storage would be similar to entity storage, you request a storage for a given type from the storage factory. We just need add a factory and an implementation that receives the entity type id in the constructor.

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

ok, think we're ready to go then!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php
    @@ -0,0 +1,152 @@
    +class NodeStatisticsDatabaseStorage implements StatisticsStorageInterface {
    

    This is the only place that writes to the node_counnter table - it'd might be nice if the schema was here too. We've done this for quite a few other tables. See \Drupal\Core\Cache\DatabaseBackend::schemaDefinition(). In this instance we could just call it in the existing hook_schema in the install but then everything is kept together. Don't put the method on the interface. At some point we'll probably introduce an interface for this so we can discover services that have schema backends.

    I don't think we can move to on demand creation because of the views integration.

    I'm not 100% certain about this - requires more discussion.

  2. +++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
    @@ -82,28 +154,64 @@ public function build() {
    +  /**
    +   * Generates the ordered array of node links for build().
    +   *
    +   * @param int[] $nids
    +   *   An ordered array of node ids.
    +   * @param string $title
    +   *   The title for the list.
    +   *
    +   * @return array
    +   *   A render array for the list.
    +   */
    +  protected function nodeTitleList(array $nids, $title) {
    +    $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
    +
    +    $items = [];
    +    foreach ($nids as $nid) {
    +      $node = $this->entityRepository->getTranslationFromContext($nodes[$nid]);
    +      $item = [
    +        '#type' => 'link',
    +        '#title' => $node->getTitle(),
    +        '#url' => $node->urlInfo('canonical'),
    +      ];
    +      $this->renderer->addCacheableDependency($item, $node);
    +      $items[] = $item;
    +    }
    +
    +    return [
    +      '#theme' => 'item_list__node',
    +      '#items' => $items,
    +      '#title' => $title,
    +      '#cache' => [
    +        'tags' => $this->entityTypeManager->getDefinition('node')->getListCacheTags(),
    +      ],
    +    ];
    +  }
    

    This change (removing the use of node_title_list()) is completely unrelated to the change afaics. It should be done in a separate issue.

Berdir’s picture

@alexpott:

1. Fine with me as long as we manually create it for now.

2.

+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -82,28 +154,64 @@ public function build() {
     if ($this->configuration['top_all_num'] > 0) {
-      $result = statistics_title_list('totalcount', $this->configuration['top_all_num']);
+      $result = $this->statisticsStorage->fetchAll('totalcount', $this->configuration['top_all_num']);
       if ($result) {
-        $content['top_all'] = node_title_list($result, $this->t('All time:'));
+        $content['top_all'] = $this->nodeTitleList($result, $this->t('All time:'));
         $content['top_all']['#suffix'] = '<br />';

The change is necessary because node_title_list() does not receive ids, it receives a query result object. We can no longer use that.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
23.63 KB
5.22 KB

Switching $result for $nids to make it a bit clearer that why we're not using node_title_list().
Moving the schema from .install to the service.

Status: Needs review » Needs work

The last submitted patch, 196: improve_statistics-1446932-196.patch, failed testing.

timmillwood’s picture

Looks like we're too early to call \Drupal::service('statistics.storage.node')->schemaDefinition() in hook_schema.

timmillwood’s picture

Based off patch in #188.
Switching $result for $nids to make it a bit clearer that why we're not using node_title_list().

mallezie’s picture

Hmm that's a strange failure.
First googling points to https://bugs.php.net/bug.php?id=50688
Lets do a retest first.

alexpott’s picture

@mallezie that's not this issues fault. That's #2762549: Drupal\field\Tests\Update\FieldUpdateTest, Drupal\views\Tests\Update\EntityViewsDataUpdateTest and Drupal\comment\Tests\CommentFieldsTest fail on 8.1.x.

So re #194 - both points I made turned out to not be good. Sorry everyone. Unfortunately this missed the 8.2.0 beta deadline. Hopefully it'll be an early commit to 8.3.x.

@Berdir are you happy with the current direction? I was wondering why no rtbc in #192.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

For some "obvious" reasons I don't disagree with the latest approach :)
It allows us to be flexible in the future, while being pragmatic now.

+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -82,28 +154,64 @@ public function build() {
+      $item = [
+        '#type' => 'link',
+        '#title' => $node->getTitle(),
+        '#url' => $node->urlInfo('canonical'),
+      ];
+      $this->renderer->addCacheableDependency($item, $node);

follow up: this could use $node->toLink()->toRenderable()

Berdir’s picture

Yes, I am, RTBC++

mallezie’s picture

Version: 8.2.x-dev » 8.3.x-dev

Setting to 8.3 and created #2778167: Build link in Statistics block with toRenderable() as a follow-up, thanks for pointing that out dawehner.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 22ab6e5 and pushed to 8.3.x. Thanks!

diff --git a/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php b/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php
index a86024b..32d2872 100644
--- a/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php
+++ b/core/modules/statistics/src/NodeStatisticsDatabaseStorage.php
@@ -1,8 +1,4 @@
 <?php
-/**
- * @file
- * Contains \Drupal\statistics\NodeStatisticsDatabaseStorage.
- */
 
 namespace Drupal\statistics;
 
diff --git a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
index df07c81..8bd84b2 100644
--- a/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
+++ b/core/modules/statistics/src/Plugin/Block/StatisticsPopularBlock.php
@@ -9,7 +9,6 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\node\Entity\Node;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\statistics\StatisticsStorageInterface;
diff --git a/core/modules/statistics/src/StatisticsViewsResult.php b/core/modules/statistics/src/StatisticsViewsResult.php
index 666acc4..ef0db97 100644
--- a/core/modules/statistics/src/StatisticsViewsResult.php
+++ b/core/modules/statistics/src/StatisticsViewsResult.php
@@ -56,4 +56,5 @@ public function getDayCount() {
   public function getTimestamp() {
     return $this->timestamp;
   }
+
 }

Coding standards fixed on commit.

alexpott’s picture

Status: Fixed » Needs work
Issue tags: +Needs change record

Didn't push - I was just checking for a change record and I realised it didn't have one. Sorry. We need one becuase this is a change - and since we need that we should apply the fixes in #205 to the patch.

alexpott’s picture

Also the issue summary should reflect the final agreed solution. If the issue summary was up-to-date the CR would be easy.

timmillwood’s picture

Issue summary: View changes
timmillwood’s picture

Status: Needs work » Needs review
alexpott’s picture

Issue summary: View changes
alexpott’s picture

Status: Needs review » Reviewed & tested by the community

Okay we have a change record and the issue summary has been updated.

  • alexpott committed ec59a51 on 8.3.x
    Issue #1446932 by timmillwood, mallezie, RobLoach, hussainweb, Wim Leers...
alexpott’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Needs change record, -Needs issue summary update

Committed ec59a51 and pushed to 8.3.x. Thanks!

Same fixes as in #205 applied on commit.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

jibran’s picture

Uploaded the patch for 8.2.x so that I can use it with 8.2.x. Please ignore this comment.

jibran’s picture

Please ignore the comment.

webchick’s picture

Issue tags: +8.3.0 release notes
webchick’s picture