Author Topic: Notes on the Conversation Bundle  (Read 582 times)

Andrew

  • Game Master / Lead Developer
  • Administrator
  • Sr. Member
  • *****
  • Posts: 1823
  • Karma: +75/-7
  • Mildly Amused
    • View Profile
    • Lemuria Community Fan Site
Notes on the Conversation Bundle
« on: November 06, 2017, 12:51:17 PM »
To make a mark as read button...

Code: [Select]
   public function markRead (ConversationMetadata $m) {
      // find the conversation in question
      $qb = $this->em->createQueryBuilder();
      $qb->select('c, msg, meta')
         ->from('MsgBundle:Conversation', 'c')
         ->join('c.metadata', 'm')
         ->leftJoin('c.messages', 'msg')
         ->leftJoin('msg.metadata', 'meta')
         ->where('m = :m')->setParameter('m', $m)
         ->andWhere($qb->expr()->orX(
            $qb->expr()->isNull('msg.id'),
            $qb->expr()->eq('msg.depth', 0),
            $qb->expr()->gt('msg.ts', 'm.last_read')
         ));
      $qb->orderBy('msg.ts', 'ASC');
      $query = $qb->getQuery();
      // set as read
      $m->setUnread(0)->setLastRead(new \DateTime("now"));

      // return true
      return true;
   }

To make a mark as read for all characters button...

Code: [Select]
   public function markReadForMany (ConversationMetadata $m) {
      // get the actual conversation id.
      $found = false;
      $myconvo = $m->getConversation();
      // get the actual player.
      $user = $m->getUser()->getAppUser()->getUser();
      // find their characters.
      foreach ($user->getCharacters() as $char) {
         // get each characters conversations
         foreach ($char->getMsgUser()->getConversationsMetadata() as $convometa) {
            if ($convometa->getConversation() == $myconvo) {
               $this->markRead($convometa);
               $found = false;
            }
         }
      }
      if ($found)
         return true;
      } else {
         return false;
      }
   }

Theoretically, that's the logic for it, but it has a big problem in that this means it's going through EVERY conversation for EVERY character a player has. Which is incredibly slow.

What we need:

1. A way to limit the characters we're searching through.
2. A way to limit the conversations we're searching through.

I toyed with some logic for this below, but it's unfinished and I'm too tired to think straight so ignore it for now.


   public function markRealmRead (ConversationMetadata $m) {
      // get the actual conversation id.
      $found = false;
      $myconvo = $m->getConversation();
      $myappref = $m->getConversation()->getAppReference();

      $query = $this->em->createQuery('SELECT c FROM MsgBundle:Conversation c JOIN cmsg_conversation_meta m ON c.id = m.conversation_id WHERE c.app_reference = :appref AND m.user in :cmsgusers);
      $query->setParameter('appref', $myappref);
      $query->setParameter('user', $cmsguser);

      $users = $m->getUser()->getAppUser()->getUser();
      foreach ($user->getCharacters() as $char) {
         foreach ($char->getMsgUser() as $cmsguser) {
           
            if ($convometa->getConversation() == $myconvo) {
               $this->markRead($convometa);
               $found = false;
            }
         }
      }
      if ($found)
         return true;
      } else {
         return false;
      }
   }
« Last Edit: November 08, 2017, 12:30:46 PM by Andrew »
Standing for the creation of interesting things since Year 1, Week 5, Day 4.
Favorite cold beverage: Strawberry Shake
My hobbies: Fixing computers, video games, anime, manga, some other stuff, sleep (in no particular order)

Andrew

  • Game Master / Lead Developer
  • Administrator
  • Sr. Member
  • *****
  • Posts: 1823
  • Karma: +75/-7
  • Mildly Amused
    • View Profile
    • Lemuria Community Fan Site
Re: Notes on the Conversation Bundle
« Reply #1 on: November 07, 2017, 02:02:01 PM »
On a different note, if you modified the NewConversationType.php Form to the following:

Code: [Select]
         if ($this->recipients) {
            $recipients = $this->recipients;
            $builder->add('contacts', 'entity', array(
               'required' => false,
               'multiple'=>true,
               'expanded'=>true,
               'label' => 'conversation.recipients.label',
               'placeholder' => 'conversation.recipients.empty',
               'class' => 'BM2SiteBundle:Character',
               'property' => 'name',
               'query_builder'=>function(EntityRepository $er) use ($recipients) {
                  $qb = $er->createQueryBuilder('c');
                  $qb->join('c.msg_user', 'u');
                  $qb->where('u IN (:recipients)');
                  $qb->andWhere('c.alive = true');
                  $qb->andWhere('c.slumbering = false');
                  $qb->orderBy('c.name', 'ASC');
                  $qb->setParameter('recipients', $recipients);
                  return $qb;
               },
            ));
         }

You would no longer display slumbering or dead characters. While the numbers are purely subjective, running this on one of my characters saw a 2.5% reduction in contacts by cutting the dead off, while removing the slumbering reduced it by an additional 65% overall.
Standing for the creation of interesting things since Year 1, Week 5, Day 4.
Favorite cold beverage: Strawberry Shake
My hobbies: Fixing computers, video games, anime, manga, some other stuff, sleep (in no particular order)

Cipheron

  • Full Member
  • ***
  • Posts: 200
  • Karma: +9/-4
    • View Profile
Re: Notes on the Conversation Bundle
« Reply #2 on: November 07, 2017, 06:22:10 PM »
Would it be better to ennumerate the characters in a conversation then to check the player vs the existing player?

Each conversation has the metas added to it:
Code: [Select]
$conversation->addMetadatum($meta);

And you can get that as an array:
Code: [Select]
foreach ($conversation->getMetadata() as $reader) {   
    if ($reader->getUser() != $author) {
        $reader->setUnread($reader->getUnread()+1);
    }


Combine that with what you seem to be doing to get the actual player:

Code: [Select]
$m->getUser()->getAppUser()->getUser();

So putting that together you could in fact work out which player the "mark read for all" is being done, then cross-check that against users of the message, something like:

Code: [Select]
$user = $m->getUser()->getAppUser()->getUser();

foreach ($myconvo->getMetadata() as $reader) {
    if ($reader->getUser()->getAppUser()->getUser() == $user) {

        // do the search for the relevant conversation here, then mark as read.

    }

To optimize the inner loop, you really need a custom SQL query. However, you don't want to mess up the code with that , so I would suggest writing a variant of this function:

Code: [Select]
$char->getMsgUser()->getConversationsMetadata()
That takes the conversationID as an actual parameter, and only returns either the required conversation or "null" (which you need to check for to make it error-proof).

Then, to optimize you can refactor things so that you keep pushing the comparision down a level of abstraction (as variants of the used functions) until you hit the level at which raw SQL queries are being written. At that point you just modify the query, and let the nice efficient database do the culling for you. Basically the way to do things more efficiently in a PHP/SQL program is to not do the database's job for it. The database is responsible for storing and searching through records to find matches.

PHP is a slow, interpreted language and you're sequentially searching records in it, which have a lot of fluff/padding. Whereas SQL is written in raw, pedal-to-the-metal c/c++/assembly by database experts. It's not just a little faster at this stuff, it's blazingly faster. Basically any time you see a "foreach" loop in the PHP that's checking whether some value is true or not and only going ahead if it is, that's very likely a place where some better-written SQL query could have avoided the entire need for the loop.
« Last Edit: November 08, 2017, 06:15:16 AM by Cipheron »

Andrew

  • Game Master / Lead Developer
  • Administrator
  • Sr. Member
  • *****
  • Posts: 1823
  • Karma: +75/-7
  • Mildly Amused
    • View Profile
    • Lemuria Community Fan Site
Re: Notes on the Conversation Bundle
« Reply #3 on: November 08, 2017, 12:29:41 PM »
So, here's the kind of weird thing about how the conversation system is setup. When you read a conversation, you're accessing that conversation based on your conversation metadata. This stores things like which messages you have access to in a given conversation.

The get user silliness is because the message system has it's own user ids, which are tied to your characters, which are tied to you. getMsgUser()->getAppUser()->getUser() is really doing "find the message system user that owns this conversation, then find that user's associated character, then get the player associated to that character."

That said, I have no problem putting SQL or DQL (Doctrine's version of SQL) into the code if there's a legitimate need for it. DQL is used a number of places, particularly in the GameRunner, which handles turns. Some forms, even the one that loads conversation targets, uses DQL to find participants. All the query builder is prettify Doctrine's own query language, which in itself just allows you to do SQL with understanding of PHP classes. I'm sure doing raw SQL would be even faster, but I don't think the gain would be worth breaking from the existing design structure.
Standing for the creation of interesting things since Year 1, Week 5, Day 4.
Favorite cold beverage: Strawberry Shake
My hobbies: Fixing computers, video games, anime, manga, some other stuff, sleep (in no particular order)

Andrew

  • Game Master / Lead Developer
  • Administrator
  • Sr. Member
  • *****
  • Posts: 1823
  • Karma: +75/-7
  • Mildly Amused
    • View Profile
    • Lemuria Community Fan Site
Re: Notes on the Conversation Bundle
« Reply #4 on: November 08, 2017, 12:41:38 PM »
Just remembered that Doctrine supports "findOneBy", which makes this shorter. Also, since there's no reason to turn php strings into variables unless we reuse them, we can cut out some rows.

Code: [Select]
   public function markReadForMany (ConversationMetadata $m) {
      // get the actual conversation id.
      $myconvo = $m->getConversation();
      // find this user's characters.
      foreach ($m->getUser()->getAppUser()->getUser()->getCharacters() as $char) {
         // see if this character has this conversation.
         if ($char->getMsgUser()->getConversationsMetadata()->findOneBy('Conversation' => $myconvo)) {
            // mark the conversation read.
            $this->markRead($char->getMsgUser()->getConversationsMetadata()->findOneBy('Conversation' => $myconvo);
         }
      }
      return true;
   }

This does the same thing as the slightly updated second bit of code in my earlier post. It will look to see what other characters of the given player have a conversation, and mark it read when this function is ran against a given conversation.
Standing for the creation of interesting things since Year 1, Week 5, Day 4.
Favorite cold beverage: Strawberry Shake
My hobbies: Fixing computers, video games, anime, manga, some other stuff, sleep (in no particular order)