Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Performance optimization and code cleanup #22

Merged
merged 6 commits into from
Nov 3, 2021

Conversation

Gadgetoid
Copy link
Collaborator

@Gadgetoid Gadgetoid commented Aug 12, 2021

This is a work in progress that rolls various suggestions from over the years into one changeset, including a rewrite to use setup.cfg for all setup business (albeit this approach is obsolete).

This replaces PRs #21 and #12.

This will probably drop Python 2.x support.

The main thrust of this change is tidyup and not performance, but I've achieved around a 2.5x speedup over even the optimized versions mostly by switching to binary IO for reading/writing pin state.

On a Pi 400 the following snippet - using GPIOPin directly to avoid extra checks and function calls - achieves a pin switch speed of roughly 160KHz:

import gpio

pin = gpio.GPIOPin(14, gpio.OUT)
print(gpio._open_pins)

value = 1

while True:
    value = 0 if value else 1
    pin.write(value)

Changes

  • Remove logging, user can print() or log this themselves
  • Support creating a GPIOPin instance directly for (slightly) faster IO
  • Rewrte IO to use bytes mode, for a ~2.5x performance gain over the optimized versions
  • Add write to match GPIOPin's write/read
  • Move all pin state handling, setup and teardown to GPIOPin
  • Remove function aliases
  • Apply flake8 linting
  • Move logging inside "_export_lock" so Export/Unexport are logged when they happen
  • Support list or tuple of pins in setup() and cleanup() for Updated README  #13 and setup() function that handles a list of pins for RPi.GPIO compatibility #10
  • Support str/int GPIO number for cleanup(pin) does not remove pin file from /sys/class/gpio #7 - costs performance but using GPIOPin directly gets it back
  • General code tidyup
  • Version bump to 1.0.0

@Gadgetoid Gadgetoid changed the title Performance optimizaton and code cleanup Performance optimization and code cleanup Aug 12, 2021
@Gadgetoid Gadgetoid force-pushed the gadgetoid-working branch 3 times, most recently from e1ba0d4 to d2b3fc9 Compare August 12, 2021 12:01
@Gadgetoid Gadgetoid mentioned this pull request Aug 12, 2021
@Gadgetoid Gadgetoid force-pushed the gadgetoid-working branch 3 times, most recently from 0b1665e to a79b974 Compare August 12, 2021 12:29
- Remove logging, user can print() or log this themselves
- Support creating a GPIOPin instance directly for (slightly) faster IO
- Rewrte IO to use bytes mode, for a ~2.5x performance gain over the optimized versions
- Add write to match GPIOPin's write/read
- Move all pin state handling, setup and teardown to GPIOPin
- Remove function aliases
- Apply flake8 linting
- Move logging inside "_export_lock" so Export/Unexport are logged when they happen
- Support list or tuple of pins in setup() and cleanup() for vitiral#13 and vitiral#10
- Support str/int GPIO number for vitiral#7 - costs performance but using GPIOPin directly gets it back
- General code tidyup
- Version bump to 1.0.0

Co-authored-by: rahulraghu94 <[email protected]>
Co-authored-by: sheffieldnick <[email protected]>
@Gadgetoid
Copy link
Collaborator Author

Running belt and braces with the new GPIOPin object in this library gets me pretty close to 200KHz on a Pi 400:

image

Code:

import gpio

pin = gpio.GPIOPin(14, gpio.OUT)

value = 1

while True:
    value = 0 if value else 1
    pin.write(value)

And the RPi.GPIO compatibility API gets around 120KHz:

image

Code:

import gpio

gpio.setup(14, gpio.OUT)

value = 1

while True:
    value = 0 if value else 1
    gpio.output(14, value)

Compared to this same code running on #21 -

image

I'd say we have a little headroom for some more hand-holding if it's needed.

Copy link
Owner

@vitiral vitiral left a comment

Choose a reason for hiding this comment

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

This looks really good, I especially like the research you put into it and changing the model to use objects, which should improve understandability and speed.

Left a few comments, feel free to discuss. After all, you're the maintainer now!

gpio/__init__.py Outdated
def __init__(self, pin, mode=None, initial=LOW, active_low=None):
existing = _open_pins.get(pin)
if existing:
existing.cleanup()
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think you want to do this on class __init__. Init should just be a way to "create" the data, it should not perform logic. The only thing you might have it do is throw an error saying it is "not setup" and directing hte user to use GPIOPin.setup

You should have a @classmethod called setup which does any setup logic you need. I think throwing an error if the pin already exists is better behavior.

All the logic around setting up the pin should be done in the setup classmethod.

Copy link
Owner

Choose a reason for hiding this comment

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

There should also be a @static called already_opened or something that simply returns the GPIOPin instance that is already open for a particular pin.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed- I wasn't sure how best to implement the case where a (relatively bonkers) user tries to mix gpio.setup() with pin = gpio.GPIOPin() but something like already_opened is a great way to work around this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(I forgot to send this, but now looking back over my rpio compat setup function your approach has some merit)

I don't necessarily agree that __init__ should not have side-effects or run logic. Where do you draw the line?

If a class involves configuring some hardware state (ie: in this case exporting a pin and opening a file descriptor) then you create an issue where you can pin = GPIOPin(n) but all other class methods fail with an error until you have also called pin.setup().

Broadly, if anything in setup must be called in order for the class to function, then this might just as well be part of __init__ or you're just creating busywork for the programmer.

Copy link
Owner

Choose a reason for hiding this comment

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

I backtracked a bit myself. I think that if you ensure singleton instances then doing the setup in __init__ is probably fine.

Returns:
str: "in" or "out"
'''
with open(os.path.join(self.root, 'direction'), FMODE) as f:
Copy link
Owner

Choose a reason for hiding this comment

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

Not needed in this PR, but it might make sense to have a get_direction and read_direction. get_direction simply returns the direction that GPIOPin believes is current, whereas read_direction actually reads the direction.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What's your impression of properties with setters instead of get and set methods? I tend to avoid them since I don't like the fact you can object.some_typo = value and quietly fail to set a property. __slots__ addresses this to some extent, I think.

Copy link
Owner

Choose a reason for hiding this comment

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

not sure about __slots__, I forget the complexity there.

Properties are fine. so you could do @property def direction(self) and def read_direction(self)

Copy link
Owner

Choose a reason for hiding this comment

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

for this review I would implement read/write direction. Then in a later review I would track that state internally and use properties.

gpio/__init__.py Outdated
'''Set the direction of pin

Args:
mode (str): "in" or "out"
Copy link
Owner

Choose a reason for hiding this comment

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

add "low" "high"

Consider using the constants instead of the strings for the docs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

"low" / "high" are technically pin states while "in" / "out" are directions/modes. Mode should probably be "direction".

gpio/__init__.py Show resolved Hide resolved
gpio/__init__.py Outdated Show resolved Hide resolved
gpio/__init__.py Outdated Show resolved Hide resolved
setup.cfg Outdated Show resolved Hide resolved
setup.cfg Outdated Show resolved Hide resolved
setup.cfg Outdated Show resolved Hide resolved
@Gadgetoid
Copy link
Collaborator Author

Thanks for the enormously thorough review. I've got some other code to bash my head against for a while, but I'll come back to this and implement your suggestions ASAP.

@vitiral
Copy link
Owner

vitiral commented Aug 12, 2021

I mean, no rush. The library has been stagnant for like 5 years...

Just glad someone's working on it :D

@Gadgetoid Gadgetoid force-pushed the gadgetoid-working branch 5 times, most recently from 123924b to 3658bc0 Compare August 12, 2021 16:30
gpio/__init__.py Outdated Show resolved Hide resolved
gpio/__init__.py Outdated Show resolved Hide resolved
gpio/__init__.py Show resolved Hide resolved
- Drop RPi.GPIO compatibility stubs
- GPIOPin: Throw RuntimeError if pin is already configured
- GPIOPin: Use FMODE for export/unexport
- GPIOPin: Implicitly cast str pin to int
- GPIOPin: new "configured" @staticmethod for retrieving existing pins
- GPIOPin: active_low now writes "0"/"1"
- GPIOPin: some docstrings + tweaks
- GPIOPin: now handles removing itself from _open_pins
- use "configured" in lieu of "set up"
- Remove duplicate code
- Allow "configured" to raise a RuntimeError directly for unconfigured pins
- Test coverage for cleanup + multi-pin cleanup
- Improve cleanup with no arguments
- Cut back cleanup to single flow, no recursion
- Store pin in class as int (this was an unecessary optimisation)
- Add setup method to GPIOPin
- Simplify gpio.setup, avoid recursion, use GPIOPin.setup
- Improve docstrings
- Test rpio equivilents of GPIOPin functions
- Test invalid pins
- Test error when trying to configure pullup
- Try to use constants in tests
- GPIOPin "mode" is now "direction"
- GPIOPin catch ValueError for implicit str to int pin conversion
- GPIOPin "active_low" "direction" is now "polarity"
- Note: Can't test python2.7 in python3 and vice versa
Copy link
Owner

@vitiral vitiral left a comment

Choose a reason for hiding this comment

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

Looking much better. A few minor nits but Approved.

gpio/__init__.py Outdated Show resolved Hide resolved
gpio/__init__.py Show resolved Hide resolved
gpio/__init__.py Outdated Show resolved Hide resolved
gpio/__init__.py Show resolved Hide resolved
- Support generators such as dict.keys()
- Add comment to explain the use of list(dict.keys()) in cleanup
- Add tests for generators
- Use None instead of '' in tests, since '' results in an empty iterable and a quiet noop in cleanup/setup
@vitiral
Copy link
Owner

vitiral commented Aug 12, 2021

I'm going to let you merge BTW. I have no other major comments. If you have concerns feel free to call them out and I'll look.

@Gadgetoid
Copy link
Collaborator Author

No worries! Thank you.

I’m going to sleep on it, and push a package up to test pypi tomorrow to make sure I’ve not done anything silly.

@Gadgetoid
Copy link
Collaborator Author

Turns out I slept on this for more than a night!

I've been wondering if it's worth adding any functions to give visibility on:

  1. What pins are actually available
  2. What pins are in use, and for what reason

Though, granted, sysfs GPIO is deprecated so I'm not sure what the benefit of expanding the scope of this project beyond a bit of a polish would be!

It could also be compelling to add libgpiod support which would be of far more ongoing utility but also a huge undertaking. A package name as succinct as "gpio" is a good opportunity to be the canonical package for ... well... gpio!

@Gadgetoid
Copy link
Collaborator Author

This was an unceremonious merge, but it's probably time to move forward!

@Gadgetoid
Copy link
Collaborator Author

Slight hiccup @vitiral - I cannot upload to https://test.pypi.org/project/gpio/

Permissions/users on test.pypi.org are discrete from those on real pypi.

No rush to fix. I have worked around this by publishing to gpio-test temporarily: https://test.pypi.org/project/gpio-test/1.0.0/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants