Efficiency of code with 100+ objects

Hi all,
I’m writing a program that has numerous moving objects on screen and my frame rate starts to take a serious hit at around 100 - 110 objects. I’ve seen programs with thousands running easily so clearly I’m writing inefficient logic.

The program uses the twitter4j library to connect to the twitter API. I am searching for all of the words from a user supplied string. When one of these words is found an object is created and animates a wave across the screen and then reverses direction while a “drip” (don’t have a better name for it) grows from the end that hit the edge of the screen.
When It reaches up over 100 objects(waves) on screen the frame rate starts to drop quite considerably, the lowest I’ve seen it is around 20fps which is disastrous for live performance.

I think the error is either in the draw loop with the removal of the dead waves or in the wave class itself as I’ve noticed that when there are a lot of “drips” is when the frame rate takes the biggest hit.

Am I on the right track for where the issue is? Is there a more efficient way to do what i’m trying to do? (Probably)

Here’s a video link of it in action showing the fluctuating frame rate:
I do understand that with the frame rate it is a requested frame rate, but when it hits 30fps it is noticeably slower.

This is as close as I can get to an MVCE, it does work, but I’ve had to take the API credentials out for security reasons.

Main Tab

import java.util.*;
import twitter4j.util.*;
import java.util.List;
import twitter4j.*;
import twitter4j.management.*;
import twitter4j.api.*;
import twitter4j.conf.*;
import twitter4j.json.*;
import twitter4j.auth.*;

TwitterStream twitterStream;

// quotes from: https://www.thoughtco.com/famous-short-quote-2833143
String uInput = "There are only two tragedies in life: one is not getting what one wants, and the other is getting it.";

ArrayList<String> words;
ArrayList<Wave> waves; 
int vertSpace;

int ySpacing;
char waveDirection;

boolean twitterInitialised = false;

void setup() {
  size(600, 1000);

  words = new ArrayList<String>();
  waves = new ArrayList<Wave>();

  userDefined = split(uInput.toLowerCase(), " ");
  vertSpace = userDefined.length;
  ySpacing = height/(vertSpace+1);
  openTwitterStream();
}

void draw() {
  background(12);

  if (int(random(10)) < 5) waveDirection = '-';
  else waveDirection = '+';


  for (int i = 0; i < words.size(); i++) {
    if (words.get(i) != null) {
      for (int n = 0; n < userDefined.length; n++) {
        if (words.get(i).equals(userDefined[n])) {
          waves.add(new Wave(ySpacing*(n+1), userDefined[n], waveDirection, int(random(200, 400))));
        }
      }
    }
  }


  for (Wave w : waves) w.draw();
  words.clear();   //clear the words array

  for (Iterator<Wave> w = waves.iterator(); w.hasNext(); ) {
    if (w.next().isDead()) {
      w.remove();
    }
  }

  fill(-1);
  textSize(12);
  text("Num Waves: " + waves.size(), 15, height-35);
  text("FPS: " + nf(int(frameRate), 0, 0), 125, height-35);
  noFill();
} 

void keyPressed() {
  if (key == ESC) {
    if (twitterInitialised) {
      twitterInitialised = false;
      twitterStream.cleanUp(); //closes down the stream
    }
    exit();
  }
}

Twitter Class

String[] userDefined;
//GetMentions men = new GetMentions();

// Stream it
void openTwitterStream() {

  // OAuth stuff
  ConfigurationBuilder cb = new ConfigurationBuilder();  
  ConfigurationBuilder b = new ConfigurationBuilder();  

  //stream api OAuth
  cb.setOAuthConsumerKey("---");
  cb.setOAuthConsumerSecret("---");
  cb.setOAuthAccessToken("---");
  cb.setOAuthAccessTokenSecret("---"); 

  b.setOAuthConsumerKey("---");
  b.setOAuthConsumerSecret("---");
  b.setOAuthAccessToken("---");
  b.setOAuthAccessTokenSecret("---");

  //the stream object
  twitterStream = new TwitterStreamFactory(cb.build()).getInstance();
 
  // filter is used to pass querys to the stream
  // see twitter4j's java doc
  FilterQuery filtered = new FilterQuery();

  // if you enter keywords here it will filter, otherwise it will sample
  String[] keywords = new String[userDefined.length];
  for (int i = 0; i < userDefined.length; i++) {
    keywords[i] = userDefined[i];
  }

  //track is like "search"... well kind of
  // for a better explanation go 
  // dev.twitter.com/streaming/overview/request-parameters"
  filtered.track(keywords);

  // the StatusListener interface is where the magic happens
  // code there will be executed upon tweets arriving
  // so we want to attach one to our stream
  twitterStream.addListener(listener);

  if (keywords.length == 0) {
    // sample() method internally creates a thread which manipulates TwitterStream 
    // and calls these adequate listener methods continuously.
    // ref //dev.twitter.com/streaming/reference/get/statuses/sample
    // "Returns a small random sample of all public statuses"
    twitterStream.sample();
  } else { 
    twitterStream.filter(filtered);
  }
  println("connected");
  twitterInitialised = true;
} 

// Implementing StatusListener interface
// the methods are pretty much self explantory
// they will be called according to the messages arrived
// onStatus is probably what you are lookikng for...
StatusListener listener = new StatusListener() {

  //@Override
  public void onStatus(Status status) {
    //println("@" + status.getUser().getScreenName() + " - " + status.getText());
    String s = status.getText().toLowerCase();
    String[] tweetWords = s.split(" ");
    for (int i = 0; i < tweetWords.length-1; i++) words.add(tweetWords[i]);
    //println("tweetWords: " + tweetWords.length);
    //tweetWords = null;
  }

  //@Override
  public void onDeletionNotice(StatusDeletionNotice statusDeletionNotice) {
    println("Got a status deletion notice id:" + statusDeletionNotice.getStatusId());
  }

  //@Override
  public void onTrackLimitationNotice(int numberOfLimitedStatuses) {
    //println("Got track limitation notice:" + numberOfLimitedStatuses);
  }

  //@Override
  public void onScrubGeo(long userId, long upToStatusId) {
    println("Got scrub_geo event userId:" + userId + " upToStatusId:" + upToStatusId);
  }

  //@Override
  public void onStallWarning(StallWarning warning) {
    println("Got stall warning:" + warning);
  }

  //@Override
  public void onException(Exception ex) {
    ex.printStackTrace();
  }
};

Wave Class


class Wave {
  PVector startPos, endPos;
  String word;
  float strokeWidth;
  int yStored;
  float bezierRadius, bezierY;
  boolean randStartEndPos, dead;
  char waveDirection;
  float opacityIter, strokeIter;
  int dripAmt, opacity, growSpd, decaySpd;

  Wave(int yPos, String word, char waveDirection, int dripAmt) {
    yStored = yPos;
    this.word = word;
    bezierY = yPos;
    startPos = new PVector(-10, yPos);
    endPos = new PVector(-10, yPos);
    strokeWidth = 1;
    randStartEndPos = false;
    dead = false;
    this.waveDirection = waveDirection;
    this.dripAmt = height/dripAmt;
    opacity = 100;
    growSpd = width/(int(random(5, 250)));
    decaySpd = width/int(random(100, 150));
    opacityIter = 0.05;
    strokeIter = 0.25;
  }

  void draw() {

    stroke(-1, opacity);
    strokeWeight(strokeWidth);
    noFill();
    bezier(startPos.x, startPos.y, startPos.x+bezierRadius, bezierY, endPos.x-bezierRadius, bezierY, endPos.x, endPos.y);
    noStroke();
    fill(210, 55, 41);

    animate();

    if (randStartEndPos) {
      stroke(-1, opacity);
      strokeWeight(strokeWidth);
      line(0, startPos.y, startPos.x, startPos.y);
    }
  }

  void animate() {
    if (endPos.x < width+5 && !randStartEndPos) {
      endPos.x+=growSpd;
    } //
    else if (endPos.x > width && startPos.x > -11) {
      if (!randStartEndPos) {
        startPos.x = random(endPos.x-20, endPos.x-10);
        endPos.x = width+5;
        bezierRadius = (endPos.x-startPos.x)/2;
        randStartEndPos = true;
      }
      strokeWidth+=strokeIter;
      opacity-=opacityIter;
      startPos.x-=decaySpd;
      if (waveDirection == '+') bezierY += dripAmt;
      else bezierY -= dripAmt;
      bezierRadius = (endPos.x-startPos.x)/2;
    } 
    if (startPos.x < -10) dead = true;
  }

  boolean isDead() {
    return dead;
  }
}

Hi! Does it run any faster or slower with

size(600, 1000, P2D);

?

Hi Abe,
No that actually appeared to make it slower.

However, I have been playing and i have found that it is the following line in the Wave Class that appears to be causing the issue:

`    bezier(startPos.x, startPos.y, startPos.x+bezierRadius, bezierY, endPos.x-bezierRadius, bezierY, endPos.x, endPos.y);`

when this is commented out the program happily will have up over 250 waves doing their thing without batting an eyelash. Though this does stop them from forming the “Drips” that I need.

Is there a better way to make a line bend as the video in the original post shows? Because my logic here is obviously not the correct approach.

I think it’s not just bezier, but the high strokeWeight (and transparency may add some slowness too). If strokeWeight is kept at 1 it does play smooth.

Maybe constructing a TRIANGLE_STRIP to draw the thick curve might be faster? In P2D? I’m not sure.

(note that the reference shows TRIANGLE_STRIP in an example but it’s missing from the list of available parameters).

This is an example that could be used to build the triangle strip. It is an optimized version of Reference / Processing.org (removed atan2, sin and cos).

size(400, 400);
noFill();
bezier(340, 80, 40, 40, 360, 360, 60, 320);
stroke(255, 102, 0);
int steps = 16;
for (int i = 0; i <= steps; i++) {
  float t = i / float(steps);
  float x = bezierPoint(340, 40, 360, 60, t);
  float y = bezierPoint(80, 40, 360, 320, t);
  float tx = bezierTangent(340, 40, 360, 60, t);
  float ty = bezierTangent(80, 40, 360, 320, t);
  float d = dist(0, 0, tx, ty);
  line(x, y, x+32*ty/d, y-32*tx/d);
}

The idea is to not call bezier() at all but to build a mesh using createShape where the points are calculated like in this example and the value 32 is modified to specify the thickness of the curve.
If the number of steps is high it can be slower, but maybe worth trying…

I’ve noticed this problem before in the past, changing the stroke weight past 1 in my software caused significant slow downs. I wasn’t sure if I had made an error somehow or if it was something else. Note it gets worse the bigger the stroke size.

I see what both of you mean, the changing stroke is a real kick in the teeth. With a stroke of 1 I can get up over 300 objects before it complains. As a way around this I think I’ll just set each object to have a random stroke weight when it is created and take out the increase of size.

I had considered doing it like somewhat like this in the first place, but I was trying to minimise the number of segments. Perhaps I should have trusted my gut. I’ll give it a whack and see how it goes.

i’ll keep you posted.

cheers.

1 Like